My Emacs Configuration

Written in org-babel, used in emacs.

This is my emacs configuration, as typed in org-babel. Its a form of literate programming which is really nice in that it basically goads me into writing a better documented emacs config.

Pre-install

The bulk of preinstallation should be accomplished automatically thanks to system-packages.

sudo add-apt-repository -y ppa:justinabrahms/ttf-cascadia-code
# This may require changing the mirror with the --only-server flag.
luarocks --local install dkjson
go get -u  github.com/sourcegraph/thyme/cmd/thyme

We have to install mu4e from source, because it's ancient on my system. (2+ years old).

# libwebkitgtk is for the `mug` tool
sudo aptitude install -y libtool autoconf libgmime-3.0-dev libxapian-dev libgtk-3-0 libwebkitgtk-3.0-dev guile-2.0-dev html2text xdg-utils
cd `mktemp -d`
wget https://github.com/djcb/mu/archive/1.2.zip
extract 1.2.zip
cd mu-1.2/
autoreconf -i
./configure
make
sudo make install

TODO s

  • implement ffap for python.
  • sql format tool

Setup package.el

package.el is a package manager for emacs modes. It is official in emacs-24, but is back ported to emacs23. The setup for single files is trivial and multi-file simple things is also pretty easy. It gets complicated if you're doing something weird like CEDET. We tie into marmalade (a package repository that doesn't require you to release your plugins under GPL) and melpa which pulls in things from github.

;; package.el
(require 'package)

(add-to-list 'package-archives
             '("melpa" . "http://melpa.org/packages/") t)
(add-to-list 'package-archives 
             '("org" . "http://orgmode.org/elpa/") t)

(package-initialize)

Get an up-to-date list of packages available to us if a cached copy doesn't exist. Then loop over the list of packages we want around and install them.

(when (not package-archive-contents)
  (package-refresh-contents))

(defvar my-packages '(clojure-mode dedicated
                            ;; elisp-cache ;; seems to be missing from marmalade
                            org paredit protobuf-mode rainbow-delimiters scpaste
                            doom-modeline ;; generates a pretty modeline
                            ;; something in ESK is breaking ido for me
                            ;; starter-kit-lisp starter-kit-js starter-kit-eshell
                            scratch dizzee ctags-update virtualenvwrapper websocket znc
                            pastels-on-dark-theme textmate pony-mode slime
                            browse-at-remote ;; open code links in external repo web ui (e.g. github)
                            company-tern ;; auto-complete via tern (if installed)
                            json-mode ;; better syntax highlighting / error reporting for json
                            nginx-mode ;; nginx syntax highlighting
                            yaml-mode
                            plan9-theme
                            haskell-mode ;; used for xmonad config
                            zencoding-mode ;; easy html authoring
                            idle-highlight-mode flymake-cursor dired-single
                            ;; flymake-less ; missing from melpa
                            git-link ;; link to line of code.
                            less-css-mode css-eldoc
                            dockerfile-mode ;; syntax highlighting for Dockerfiles
                            web-mode ;; multi-mode [sorta] for html files
                            pastels-on-dark-theme ;; dark and purple
                            terraform-mode
                            lua-mode ;; for awesome-wm, but generally for lua.
                            textmate pony-mode
                            fill-column-indicator
                            auto-complete scss-mode flymake-sass
                            htmlize ;; used for blog publishing
                            ; skipping for now, due to installation error.
                            ; pymacs ;; python + emacs bridge for ropemacs refactoring
                            ;; dired-details ;; makes dired buffer nice -- has since disappeared from package.el repos
                            yasnippet ;; programmable tab-completion
                            xclip ;; linux console copy+paste goodness
                            gnuplot ;; used for gnuplot graph exports from org-babel's publish
                            enh-ruby-mode ;; better ruby mode
                            inf-ruby ;; irb in a separate pane
                            rspec-mode ;; ruby testing mode
                            projectile ;; project based nativation
                            graphviz-dot-mode ;; graphviz syntax highlighting
                            ensime ;; scala editing
                            jdee ;; java editing
                            use-package ;; package declaration that's nicer to work with
                            visual-fill-column ;; visual-line-mode obeys fill-column
                            llvm-mode ;; because I occasionally make poor decisions
                            )
  "A list of packages to ensure are installed at launch.")

  (if (not (equal window-system 'w32))
      (add-to-list 'my-packages
                           'pymacs ;; python + emacs bridge for ropemacs refactoring
                           ))

    (if (equal window-system 'ns)
        (push "/Applications/Emacs.app/Contents/MacOS/bin" exec-path))

Loop through and install the packages. Sometimes there are errors. Ignore those, because it's not actually helpful to block the initial setup.

(dolist (p my-packages)
  (when (not (package-installed-p p))
    (ignore-errors (package-install p))))

I've started using use-package, which simplifies adding packages. This means I'll be migrating things out of the list above and into their own sections of code, which I think will make things simpler. As such, let's setup use-package.

(eval-when-compile (require 'use-package))

We also want our packages to be installed by default. Provided we're using use-package correctly, I believe it doesn't load them until it needs them, but that functionality doesn't work if we don't have the packages to begin with.

(require 'use-package-ensure)
(setq use-package-always-ensure t)

Let's also keep our packages up to date, which will probably have No Adverse Affects™.

(use-package auto-package-update
  :config
  (setq auto-package-update-delete-old-versions t)
  (setq auto-package-update-hide-results t)
  (auto-package-update-maybe))

There's an extension to use-package which will let you install system packages. This has significantly reduced the setup time for packages I use.

(use-package use-package-ensure-system-package)

To get our paths setup correctly (for test running, etc), pull our exec-path out of our shell settings.

(use-package exec-path-from-shell)
(exec-path-from-shell-initialize)

Some packages require specific emacs versions, so provide a helper function for later.

(defun emacs-version-gt-p (at-least-major at-least-minor)
  (let* ((version-arr (split-string emacs-version "\\."))
         (major (string-to-number (nth 0 version-arr)))
         (minor (string-to-number (nth 1 version-arr))))
    (if (> major at-least-major)
        t
      (if (and (equal major at-least-major) (>= minor at-least-minor))
          t
        nil))))

Packages not on ELPA

There aren't many packages that aren't installable via package.el. The most notable one is Martin Blais's beancount script which helps with finances.

(defun load-if-exists (f)
  (if (file-exists-p (expand-file-name f))
      (load-file (expand-file-name f))))

(load-if-exists "~/src/bitbucket.org/blais/beancount/src/elisp/beancount.el")

System dependencies

This is currently fairly ubuntu-specific, as that's the only operating system I run today. Most package updates should be handled by :ensure-system-package, but there are some things that aren't for particularly new modes or similar.

(use-package system-packages)

(defvar my-system-packages
  '(xclip ; clipboard tooling
    autojump ; quickly navigate directories in terminal
    texlive texlive-latex-extra texinfo ; tex for tangling PDFs
    fx ; it's like jq, but better & iterative
    source-highlight ; makes my less & diff color coded in terminals
    python3-pip ; python3, non-default on ubuntu systems
    zsh zsh-doc ; shell that I use
    virtualenvwrapper 
    git htop w3m aptitude unity-tweak-tool tree exuberant-ctags ; misc utilities
    gnuplot-x11 ; for graphs (used in blog and elsewhere)
    shellcheck ; used in flymake for shell scripts
    imapfilter offlineimap lua5.3 luarocks ; email
    x11-utils wmctrl xdotool ; used by thyme
    ;; fonts
    unifont ttf-ancient-fonts fonts-inconsolata xfonts-terminus
    ttf-cascadia-code ttf-anonymous-pro fonts-hack-ttf))

(require 'subr-x)
(system-packages-install (string-join (mapcar 'symbol-name my-system-packages) " ") " -y")
  

Generic Emacs Stuff

Emacs has a few things that nearly everyone changes. Minimize the chrome (emacs should be as command-line-like as possible), lose the overly-verbose and annoying prompts, etc. These are all documented inline.

(defalias 'qrr 'query-regexp-replace)
(fset 'yes-or-no-p 'y-or-n-p)  ;; only type `y` instead of `yes`
(setq inhibit-splash-screen t) ;; no splash screen
(setq-default indent-tabs-mode nil)      ;; no tabs!
(setq fill-column 80) ;; M-q should fill at 80 chars, not 75
(setq initial-buffer-choice "~/Dropbox/docs/eng-log.org") ;; make the eng log the first file that's open.

;; general programming things
(menu-bar-mode -1) ;; minimal chrome
(tool-bar-mode -1) ;; no toolbar
(scroll-bar-mode -1) ;; disable scroll bars
(set-frame-font "Cascadia Code-11") ;; Mmm. Delicious fonts.
;; other fonts I like: proggy clean, inconsolata, terminus.
(setq-default truncate-lines 1) ;; no wordwrap

(require 'uniquify)
(setq uniquify-buffer-name-style 'post-forward)  ;; buffernames that are foo<1>, foo<2> are hard to read. This makes them foo|dir  foo|otherdir
(setq desktop-load-locked-desktop "ask") ;; sometimes desktop is locked, ask if we want to load it.
(desktop-save-mode 1) ;; auto-save buffer state on close for a later time.
(setq abbrev-file-name "~/.emacs.d/abbrev_defs") ;; where to save auto-replace maps

(require 'doom-modeline)
;; note, if the fonts are weird, run `M-x all-the-icons-install-fonts`
(doom-modeline-mode 1) ;; enable pretty modeline

;; colorize the output of the compilation mode.
(require 'ansi-color)
(defun colorize-compilation-buffer ()
  (toggle-read-only)
  (ansi-color-apply-on-region (point-min) (point-max))

  ;; mocha seems to output some non-standard control characters that
  ;; aren't recognized by ansi-color-apply-on-region, so we'll
  ;; manually convert these into the newlines they should be.
  (goto-char (point-min))
  (while (re-search-forward "\\[2K\\[0G" nil t)
    (progn
      (replace-match "
")))
  (toggle-read-only))
(add-hook 'compilation-filter-hook 'colorize-compilation-buffer)
  

Zooming text is useful for presentations.

(defun djcb-zoom (n)
  "with positive N, increase the font size, otherwise decrease it"
  (set-face-attribute 'default (selected-frame) :height
                      (+ (face-attribute 'default :height) (* (if (> n 0) 1 -1) 10))))


(global-set-key (kbd "C-+")      '(lambda nil (interactive) (djcb-zoom 1)))
(global-set-key (kbd "C--")      '(lambda nil (interactive) (djcb-zoom -1)))

Below are a few kill-ring related changes for nice pasting. Pulled from this thread on pasting errors on linux.

(setq kill-ring-max 100) 
(setq x-select-enable-clipboard t) 
(setq select-active-regions t) 
(setq save-interprogram-paste-before-kill 1) 
(setq yank-pop-change-selection t) 

My browser preference changes from time to time, but we tell emacs about the current flavor of the month here. This ensures when I open links with M-x browse-url that it opens in the correct browser..

(setq
 browse-url-browser-function 'browse-url-generic
 browse-url-generic-program "firefox")

Some variables are file-specific and are denoted in a header function. I allow those in particular so that loading them in daemon mode doesn't prompt me. The mechanism here is to provide a callable that checks the value of that variable.

(put 'encoding 'safe-local-variable (lambda (val) #'stringp))
(put 'org-src-preserve-indentation 'safe-local-variable (lambda (val) #'booleanp))

Helpful elisp things.

There's no clear support for adding multiple things to a list via add-to-list, so this does that.

(defun ja/add-to-list-multiple (list to-add)
  "Adds multiple items to LIST.
Allows for adding a sequence of items to the same list, rather
than having to call `add-to-list' multiple times."
  (interactive)
  (dolist (item to-add)
    (add-to-list list item)))

Lisp

Paredit is a really neat lisp editing mode. One big thing it does for you is keep your parens balanced at all times, which turns out to be pretty important. It has a lot of mini-refactoring commands built-in (pull up, add parameter, etc) but I always forget them because I don't write lisp enough to commit it to memory.

eldoc mode is will update your minibuffer to show the parameters the function under your cursor takes, which can be a helpful for jogging your memory.

(eval-after-load 'paredit
  ;; need a binding that works in the terminal
  '(define-key paredit-mode-map (kbd "M-)") 'paredit-forward-slurp-sexp))

(show-paren-mode 1)  ;; highlight matching parenthasis
(add-hook 'emacs-lisp-mode-hook 'paredit-mode)

;; nifty documentation at point for lisp files
(add-hook 'emacs-lisp-mode-hook 'turn-on-eldoc-mode)
(add-hook 'lisp-interaction-mode-hook 'turn-on-eldoc-mode)
(add-hook 'ielm-mode-hook 'turn-on-eldoc-mode)

Slime is the big part of a lisp IDE. Its a process that runs an inferior process (usually a lisp interpreter) in the background and you can send information to it.

(require 'slime)

Python

There's a weird history with python and emacs. The FSF maintains a copy of python-mode which ships with emacs. The Python community maintains a separate version. They have evolved away from each other and each supports different things. I'm currently using the FSF version, but I'm not sold on it quite yet. I've run into a few syntax highlighting bugs where the buffer won't fully fill out. I'm now using python.el, as it works with yasnippet and indentation seems to be a bit better.

  ;; python
  (add-hook 'python-mode-hook (lambda () 
                                ;; This breaks the blog export, as the
                                ;; python snippet doesn't actually have
                                ;; a filename. Need to investigate
                                ;; flycheck for options. We'll just
                                ;; spawn a new emacs without this
                                ;; enabled for now.
                                (setq fill-column 80)
                                (fci-mode 1)))
  
  (add-to-list 'auto-mode-alist '("\\.py" . python-mode))

Virtualenv is a tool in the python community which sorts out your Python package dependencies into their own contained enviroments. This is similar to RVM and friends in the ruby community. virtualenvwrapper is a minor-mode which helps you operate within these from within emacs. It is pretty good!

(require 'virtualenvwrapper)
(venv-initialize-interactive-shells) ;; if you want interactive shell support
(venv-initialize-eshell) ;; if you want eshell support
(setq venv-location (expand-file-name "~/.virtualenvs/"))

Pony-mode is a Django helper mode which gives you access to many neat commands like runserver, manage, tests and more from handy keybindings. This is a small patch for the project which will take into account an directory which contains all of your apps and properly filter it out when determining app names.

  (setq pony-app-dir-prefix "apps")
  
  (defun pony-get-app ()
    "Return the name of the current app, or nil if no app
  found. Corrects for excluded prefix."
    (let* ((root (pony-project-root))
       (excluded-prefix (if (not (= (length pony-app-dir-prefix) 0)))
                    (concat root pony-app-dir-prefix "/")
                  root))
           (re (concat "^" (regexp-quote excluded-prefix) "\\([A-Za-z_]+\\)/"))
           (path (or buffer-file-name (expand-file-name default-directory))))
      (when (string-match re path)
        (match-string 1 path)))
  
  (defun pony-time ()
    "Helper function to get an immediate working setup after a reboot."
    (interactive)
    (if virtualenv-workon-session
        (progn
          (pony-runserver)
          (pony-manage-run '("celeryd" "-lINFO" "--traceback" "--autoreload"))
          (pony-shell)
          (sql-mysql))
      (error "setup your virtualenv first")))

I rely on having a few utilities around in python land, so let's track a requirements file.

coverage
rope
ropemacs
-e git+https://github.com/offbyone/Pymacs@fix-python_load_helper-call#egg=Pymacs

Ropemacs is a binding to rope, which supports refactoring of Python code. It turns out that pymacs isn't super well maintained. A patch since 2013 still hasn't landed. We use offbyone's fork instead (which, unfortunately, isn't super well maintained either.). It all works though.

There was an additional step wherein I had to go to the checkout location of Pymacs and type make to have it compile the correct things. Rope also shadows my recompile key (C-c g), so disable that.

  (ignore-errors
    (progn
      (require 'pymacs)
      (setq pymacs-python-command (expand-file-name "~/.virtualenvs/emacs/bin/python"))
      (pymacs-load "ropemacs" "rope-")
      (setq ropemacs-guess-project t)  ; don't prompt for project, try to figure it out.
      (setq ropemacs-enable-shortcuts nil) ; don't shadow C-c g for recompile.
      ;; Help found at http://stackoverflow.com/a/6806217/4972
      (eval-after-load "ropemacs-mode"
        (define-key ropemacs-local-keymap (kbd "C-c g") nil))))

Python-mode doesn't play well with electric-indent-mode. Instead of properly indenting things, it adds an extra level of indentation when you press RET. This is infuriating, so we turn it off.

;;; Indentation for python

;; Ignoring electric indentation
(defun electric-indent-ignore-python (char)
  "Ignore electric indentation for python-mode"
  (if (equal major-mode 'python-mode)
      'no-indent
    nil))
(add-hook 'electric-indent-functions 'electric-indent-ignore-python)

;; Enter key executes newline-and-indent
(defun set-newline-and-indent ()
  "Map the return key with `newline-and-indent'"
  (local-set-key (kbd "RET") 'newline-and-indent))
(add-hook 'python-mode-hook 'set-newline-and-indent)

I enjoy pdbpp, a tool which is an improvement to python's own pdb. The biggest feature for me is sticky mode, which shows you more context via curses. This config will enable it by default.

import pdb

class Config(pdb.DefaultConfig):
    sticky_by_default = True # start in sticky mode

Interactive Shell prompts

A few configurations and custom defined shell methods for eshell. Eshell is a terminal replacement implemented entirely in elisp. This sounds weird. It is weird. It has the benefit of having elisp as a first class language so you can do things like: cat foo/bar/baz > (switch-to-buffer "*test*") which opens the file contents in a new buffer names *test*.

(setq path-to-etags "/usr/bin/etags")

;; if OSX...
(if (equal window-system 'ns)
    (progn
      (push "/Applications/Emacs.app/Contents/MacOS/bin" exec-path)
      (setq path-to-etags "/Applications/Emacs.app/Contents/MacOS/bin/etags")))

(defun if-string-match-then-result (to-match pairs)
  "Takes a string to match and a list of pairs, the first element
  of the pairs is a regexp to test against the string, the second of
  which is a return value if it matches."
  (catch 'break
    (dolist (val pairs)
      (if (string-match-p (car val) to-match)
          (progn
            (throw 'break (cadr val)))))
    (throw 'break nil)))

(setq eshell-history-size nil) ;; sets it to $HISTSIZE

(defun eshell/extract (file)
  (eshell-command-result (concat (if-string-match-then-result
                                  file
                                  '((".*\.tar.bz2" "tar xjf")
                                    (".*\.tar.gz" "tar xzf")
                                    (".*\.bz2" "bunzip2")
                                    (".*\.rar" "unrar x")
                                    (".*\.gz" "gunzip")
                                    (".*\.tar" "tar xf")
                                    (".*\.tbz2" "tar xjf")
                                    (".*\.tgz" "tar xzf")
                                    (".*\.zip" "unzip")
                                    (".*\.jar" "unzip")
                                    (".*\.Z" "uncompress")
                                    (".*" "echo 'Could not extract the requested file:'")))
                                 " " file)))

(defun mass-create-eshells (names)
  "Creates several eshells at once with the provided names. Names
  are surrounded in astrisks."
  (dolist (name names)
    (let ((eshell-buffer-name (concat "*" name "*")))
      (eshell))))

(defun eshell/clear ()
  "clear the eshell buffer."
  (interactive)
  (let ((inhibit-read-only t))
    (erase-buffer)))

(defun eshell/mcd (dir)
  "make a directory and cd into it"
  (interactive)
  (eshell/mkdir "-p" dir)
  (eshell/cd dir))

(defun eshell/git-delete-unreachable-remotes ()
  "Delete remote git branches which have been merged into master"
  (interactive)
  (if (not (string-equal "master" (magit-get-current-branch)))
      (message "Not on master. This probably doesn't do what you want."))
  (shell-command "git branch -r --merged | grep -v '/master$' | sed -E 's/origin\\/(.*)/:\\1/' | xargs git push origin"))

Eshell prompts can get a bit long. This shortens them.

(defun abrahmsj/eshell--shorter-path (path max-len)
  "Return a potentially trimmed-down version of the directory PATH, replacing
parent directories with their initial characters to try to get the character
length of PATH (sans directory slashes) down to MAX-LEN."
  (let* ((components (split-string (abbreviate-file-name path) "/"))
         (len (+ (1- (length components))
                 (cl-reduce '+ components :key 'length)))
         (str ""))
    (while (and (> len max-len)
                (cdr components))
      (setq str (concat str
                        (cond ((= 0 (length (car components))) "/")
                              ((= 1 (length (car components)))
                               (concat (car components) "/"))
                              (t
                               (if (string= "."
                                            (string (elt (car components) 0)))
                                   (concat (substring (car components) 0 2)
                                           "/")
                                 (string (elt (car components) 0) ?/)))))
            len (- len (1- (length (car components))))
            components (cdr components)))
    (concat str (cl-reduce (lambda (a b) (concat a "/" b)) components))))

(defun abrahmsj-eshell-prompt-function ()
  (concat (abrahmsj/eshell--shorter-path (eshell/pwd) 40)
          (if (= (user-uid) 0) " # " " $ ")))

(setq eshell-prompt-function 'abrahmsj-eshell-prompt-function)

This allows me to swap the eshell buffer to the directory of my current buffer.

(defun eshell-cwd ()
  "
  Sets the eshell directory to the current buffer

  Usage: M-x eshell-cwd 
  "
  (interactive)

  (let (
        (path (file-name-directory (or  (buffer-file-name) default-directory)))
       )
    (progn
      (with-current-buffer "*eshell*"
        (cd path)
        (eshell-emit-prompt))
      (switch-to-buffer "*eshell*"))))

And this allows for setting a PAGER that's valid for use within eshell. Otherwise, you get a prompt about how eshell isn't a good enough terminal to run less.

;; Taken from https://www.reddit.com/r/emacs/comments/8z9fp8/how_so_you_use_eshell/e2hprla/
;; https://github.com/cadadr/configuration/blob/master/emacs.d/extras/eless.sh
;; https://github.com/cadadr/configuration/blob/5b4f17d140728236056dde4b1f0c7f139fd10b94/emacs.d/init.el#L668

(defun gk-less (fifo)
  "Companion function for ‘extras/eless.sh’."
  (let ((buf (generate-new-buffer "*pager*")))
    (make-process
     :name "gk-pager" :buffer buf :command `("cat" ,fifo)
     :sentinel #'gk-less--proc-sentinel
     :filter #'gk-less--proc-filter)
    (view-buffer buf 'kill-buffer)))

(defun gk-less--proc-sentinel (proc string)
  (ignore proc string))

(defun gk-less--postprocess (proc)
  (goto-char (point-min))
  (cond
   ;; Man pages:
   ((save-excursion (search-forward "" nil t))
    (Man-fontify-manpage))
   ;; Diffs:
   ((save-excursion
      (and (looking-at "^diff")
           (re-search-forward "^---" nil t)
           (re-search-forward "^@@" nil t)))
    (diff-mode))
   (:else
    (special-mode))))

(defun gk-less--proc-filter (proc string)
  (let ((buf (process-buffer proc))
        (mark (process-mark proc)))
    (with-current-buffer buf
      (let ((buffer-read-only nil))
        ;; make sure point stays at top of window while process output
        ;; accumulates
        (save-excursion
          (goto-char mark)
          (insert string)
          (ansi-color-filter-region mark (point))
          (set-marker mark (point)))
        ;; Post-processing the buffer:
        (unless (process-live-p proc)
          (gk-less--postprocess proc))))))

(setenv "PAGER" "eless.sh")

And the shell script that makes it work.

# A script for $PAGER that redirects to an Emacs buffer
# Adapted from https://crowding.github.io/blog/2014/08/16/replace-less-with-emacs/

set -e

# make a named fifo
FIFO=$(mktemp -ut pagerXXXXXXXXXXX.$$)
mkfifo $FIFO

emacsclient -u -e "(gk-less \"$FIFO\")"

exec cat > "$FIFO"

When eshell isn't quite the thing I want, vterm allows me to use what feels like a real terminal within emacs.

(use-package vterm
  ;; Note this requires cmake 3.11. Ubuntu 18.04 only has 3.10.
  ;; Install it via https://askubuntu.com/a/1157132/1105 if you must
  :ensure-system-package (cmake
                          (libtool . "libtool-bin")))

Javascript

Some generic javascript setup. There's a really neat thing called slime-js which I haven't setup yet. It allows you to have a slime process tied to a javascript REPL. The uptick of this is that you can also have that REPL tied to chrome's web inspector so the javascript you evaluate in it are also in the context of the currently opened webpage. I'm not yet sure how this will work in the context of our backbone app which uses closures everywhere, but we'll see.

(use-package js2-mode
  :mode ("\\.js" . js2-mode)
  :init
  (setq js2-global-externs '("it" "afterEach" "beforeEach" "before" "after" "describe" "require" "module"))

  ;; todo: I think js2-refactor-mode should go in it's own use-package?
  ;; :hook (js2-imenu-extras-mode
  ;;     add-node-modules-path
  ;;     js2-refactor-mode
  ;;     flycheck-mode)

  :config
  (setq-default js2-basic-offset 2)
  (setq js-indent-level 2))

(use-package json-mode
  :mode ("\\.json" . json-mode))


(use-package js2-refactor
  :requires js2-mode)

(use-package prettier-js
  :requires js2-mode)

(add-hook 'js2-mode-hook 'nvm-use-for-buffer)
(add-hook 'js2-mode-hook 'add-node-modules-path)
(add-hook 'js2-mode-hook 'prettier-js-mode)
;; (add-hook 'js2-mode-hook 'cov-mode)


;; TODO: It would be great to have flycheck enabled on json-mode to
;; detect json errors, but there's no hooks in the module today.

;; Javascript setup via https://emacs.cafe/emacs/javascript/setup/2017/04/23/emacs-setup-javascript.html
;; (js2r-add-keybindings-with-prefix "C-c C-r")


;; autocompletion via tern via https://emacs.cafe/emacs/javascript/setup/2017/05/09/emacs-setup-javascript-2.html
(use-package company-tern
 :ensure-system-package 
 ((tern . "npm install -g tern"))
 :after js2-mode
 :hook '((js2-mode . company-mode)
         (js2-mode . tern-mode))
 :config
 (add-to-list 'company-backends 'company-tern)
 (define-key tern-mode-keymap (kbd "M-.") nil)
 (define-key tern-mode-keymap (kbd "M-,") nil)
 ;; we're going to use js2-refactor mode for renames.
 (define-key tern-mode-keymap (kbd "C-c C-r") nil))

;; (require 'slime-js)

(defun find-imports (ext import-syntax-fn root tag)
  "Searches for occurrences of `tag` in files under `root` with extension `ext`

    Slightly confusing bash command which will search for java
    imports in your `get-java-project-root` directory and present you
    with a list of options sorted in most-used order. It does not
    insert them into the buffer, however.

    import-syntax-fn is a fn, given a tag, which returns an line of import code.

    returns a list of strings indicating used imports, most used first
    "


  (let* ((command (concat
                       ;;; find all java files in project root (excluding symlinks)
                   "find -P " root " -name '*." ext "' -type f | "
                       ;;; filter out imports that match tag
                   "xargs grep -h '" (funcall import-syntax-fn tag) "' "
                       ;;; group occurrences, count unique entries, then sort DESC
                   " | sort | uniq -c | sort -nr "
                       ;;; trim whitespace and ditch the count
                   " | sed 's/^\s*//' | cut -f2- -d ' '"))
         (results (shell-command-to-string command)))
    (progn
      (message command)
      (if (not (eq 0 (length results)))
          (split-string
           results
           "\n" t)))))

(defun copy-js-imports ()
  (interactive)
  (kill-new
   (first (find-imports "js" 
                        (lambda (tag) (concat tag " = require")) 
                        (textmate-project-root) (thing-at-point 'word)))))

A handy utility that will take a region and format is as JSON with nice indentation.

  (defun pretty-print-json(&optional b e)
    "Shells out to Python to pretty print JSON" 
    (interactive "r")
    (shell-command-on-region b e "python -m json.tool" (current-buffer) t)
  )

I attempted to setup javascript support within ctags using romainl/ctags-patterns-for-javascript, but it never actually worked for me, unfortunately. Perhaps worth trying again eventually.

Ctags happens to generate really large files. Let's disable the warning around opening large files (including TAGS files) unless its more than 50mb or so.

(setq large-file-warning-threshold 50000000) ;; 50mb

I use nvm for work which manages node versions and a node path. Teach emacs about that path. This auto-loads and there are no interactive functions, but it puts node on your path.

(use-package nvm
  :demand)

(if (file-exists-p (expand-file-name "~/.nvm"))
  (nvm-use "v10.15.3")) ;; default

We also want nodemodules on our path, because lots of juicy binaries live there.

(use-package add-node-modules-path
  :requires js2-mode)

Typescript

I do a small amount of typescript at work.

(setq typescript-indent-level 2)

Language servers

I'm going to experiment with language servers, using lsp-mode specifically. This is a pretty nice recent trend in languages, so you can externalize a lot of the particularities of programming languages.

(use-package lsp-mode
  :init (setq lsp-keymap-prefix "C-;")
  :hook ((js2-mode . lsp)
         (yaml-mode . lsp)
         (lsp-mode . lsp-enable-which-key-integration))
  :commands lsp lsp-deferred
  :ensure-system-package
  ((typescript-language-server . "npm install -g typescript-language-server")
   (javascript-typescript-langserver . "npm install -g javascript-typescript-langserver")
   (yaml-language-server . "npm install -g yaml-language-server")
   (tsc . "npm install -g typescript")))

(use-package lsp-ui
  ;; flycheck integration & higher level UI modules
  :commands lsp-ui-mode)

(use-package company-lsp
  ;; company-mode completion
  :commands company-lsp
  :config (push 'company-lsp company-backends))

(use-package lsp-treemacs
  ;; project wide overview
  :commands lsp-treemacs-errors-list)

(use-package dap-mode
  :commands (dap-debug dap-debug-edit-template))

(use-package which-key
  :config (which-key-mode))

CSS & other general bits.

CSS mode is pretty well done. Just change the indentation to 2 spaces rather than 4.

  (setq css-indent-offset 2)

Linting is important in all languages, even CSS.

; (require 'flymake-less)
(require 'css-eldoc)

web-mode is an interesting new mode which bridges the gap with mixed-content template code. You get handy html syntax highlighting and basic controls, while simultaneously getting some help in the template code. This mostly manifests as control structures, pairing of open parens, etc.

(require 'web-mode)
(add-to-list 'auto-mode-alist '("\\.hb\\.html\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.phtml\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.tpl\\.php\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.jsp\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.as[cp]x\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.erb\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.html\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.hbs\\'" . web-mode))

;; everything is indented 2 spaces
(setq web-mode-markup-indent-offset 2)
(setq web-mode-css-indent-offset 2)
(setq web-mode-code-indent-offset 2)

Java

I programmed Java with Emacs at Google on and off for 2 years (swapping between Eclipse on occasion). Thanks to some awesome tools they have internally, it was pretty great. Similar to programming Python in emacs with an up-to-date TAGS file. I don't know that I'd do it outside of Google beyond a super tiny project, but the slowness of the custom eclipse plugin they had was just really difficult for me to cope with.

(add-hook 'java-mode-hook (lambda ()
                            (setq c-basic-offset 2)
                            (setq fill-column 100)
                            (fci-mode t)
                            (subword-mode t)
                            (local-set-key (kbd "C-M-h") 'windmove-left)
                            (hs-minor-mode 1))
          )

I'm trying out JDEE mode, but am still unsure about it. This points jdee-mode to a JAR of the JDEE server. This assumes you've done the maven instructions on their repo.

(setq jdee-server-dir
      (concat
       (getenv "HOME")
       "/src/github.com/jdee-emacs/jdee-server/target"))

Clojure

I've been playing a bit with clojure since folks at work use it.

(use-package cider)

SQL

sql-postgres mode must defer passwords to a special, postgres-specific password file because postgres doesn't allow you to specify passwords on the command line (presumably due to security reasons). As such, we pass the -w flag to tell it to look for a password file.

(setq sql-postgres-options '("-P" "pager=off" "-w"))

Markdown

Live-markdown previews are quite useful when editing large documents. flymd offers live preview, but doesn't work for org-mode source regions natively. Because of this, we need to change their "is this a markdown file?" regex slightly to include the string "markdown".

(use-package markdown-mode
  :commands markdown-mode
  :ensure-system-package (markdown pandoc)
  :config
  (setq flymd-markdown-regex (mapconcat 'identity '("\\.md\\'" "\\.markdown\\'" "markdown") "\\|"))

  ;; The default command for markdown (~markdown~), doesn't support tables
  ;; (e.g. GitHub flavored markdown). Pandoc does, so let's use that.
  (setq markdown-command "pandoc --from markdown --to html")
  (setq markdown-command-needs-filename t))

(use-package flymd
  :hook markdown-mode
  :commands flymd-flyit
  :requires markdown-mode)

Kubernetes

I've been poking around with k8s for some personal projects. I've heard wonderful thinks about kubernetes.el, so I figured I'd try it.

(use-package kubernetes)

golang

Go is nice enough to ship with a formatter. The least we could do is run it. Furthermore, there are helpful plugins around showing the method parameters via eldoc. This requires us to set up our exec-path to point to the gocode binary, which can be found here. Following along with gocode, that library provides an autocomplete setup, which I'd like to use.

;; based on the assumption that the following repos are installed and
;; available on exec-path.
;; 
;; - github.com/nsf/gocode
;; - golang.org/x/tools/cmd/goimports
;; - github.com/rogpeppe/godef
;; - github.com/golang/lint

(use-package go-eldoc
  :requires go-mode
  :hook go-mode)

;; (use-package go-autocomplete)
(use-package gotest
  :hook go-mode
  :requires go-mode)

;; TODO(abrahms): maybe gopath isn't setup??
(use-package golint
  :requires go-mode
  :hook go-mode
  :init
  (add-to-list 'load-path (expand-file-name "~/src/golang.org/x/lint/misc/emacs"))

  :ensure-system-package 
  ((golint . "go get -u golang.org/x/lint/golint")))

(use-package go-autocomplete
  :hook go-mode
  :requires go-mode
  :init
  ;; setting up autocomplete should happen after yasnippet so we don't duplciate tab bindings.
  (require 'auto-complete-config))

(use-package go-mode
  :commands go-mode
  :ensure-system-package
  ((goimports . "go get -u golang.org/x/tools/cmd/goimports")
   (godef . "go get -u github.com/rogpeppe/godef")
   (gocode . "go get -u github.com/nsf/gocode"))
  :config
  (setq gofmt-command "goimports")
  (add-hook 'before-save-hook #'gofmt-before-save)
  (add-hook 'go-mode-hook 'go-eldoc-setup))

Jumping around in source code is quite helpful, but let's shadow normal ctags support.

(defun ja-gomode-hook ()
  (if not (string-match "go" compile-command)
    (set (make-local-variable 'compile-command)
         "go generate && go build -v && go test -v --coverprofile=cover.out && go vet"))
  (local-set-key (kbd "M-.") 'godef-jump))
(add-hook 'go-mode-hook 'ja-gomode-hook)

Emacs Built-ins

tramp

Tramp is one of those features that you don't really make use of in the beginning, but as you get more familiar with it, the more indespensible it is. Tramp allows you to edit files on remote servers as if they were on your local machine. From the find-file prompt, you can type things like: /ssh:user@host:/home/user/myfile.txt which will ssh in to host as user and open up myfile.txt in emacs. When you save, changes are pushed back to the remote host. You can also edit files as root (I do it via sudo) like /sudo:host:/etc/nginx/nginx.conf

If I access something via root@host, actually ssh into the service using my default username (which is the username of my current system user) and sudo to root. I disable root access on my servers (Ubuntu default) which stops a reasonable number of attacks.

  (use-package tramp
    :config
    ;; NB: I had to remove this b/c I couldn't do /sudo::/ on my localhost if ssh wasn't setup
    ; if I use tramp to access /ssh:root@..., then actually ssh into it
    ; and sudo, not login as root.
    ;; (set-default 'tramp-default-proxies-alist (quote ((".*" "\\`root\\'" "/sudo:%h:"))))
    )

server-mode

Emacs has this really interesting feature called server-mode. Emacs is notoriously slow to start (this happens if you have a giant emacs config that does stupid things). To combat this, you can start a single server process which will accept multiple clients. The server maintains the state of everything (files open, variables defined, processes running) and your client can attach / disconnect as necessary. The connecting is super fast (vim speeds).

(if (not server-mode)
    (server-start nil t))

encryption mode

I keep a file around of encrypted passwords that emacs needs to know about (simple stuff like my znc-password to connect to my IRC server). I store that in a gpg encrypted file. Thankfully, emacs has nifty ways of building that stuff in.

  (require 'epa)
  (epa-file-enable)
  (setq epg-gpg-program "gpg")


  (load-if-exists "~/.emacs.d/secrets.el.gpg")
  (load-if-exists "~/.emacs.d/secrets.el")

ERC

ERC is an IRC mode for emacs. Its nothing special. ZNC is a plugin which makes it simpler to connect to a ZNC server. ZNC is an IRC bouncer, which is a long-running process which keeps you on IRC. You can join and quit as you like, but you stay online throughout. Very similar to emacs's server-mode. Thanks to @bitprophet for letting me use his ZNC server.

  (use-package znc
    :if (boundp 'znc-password)
    :init
    (setq znc-servers
          `(("justin.abrah.ms" 5000 t
             ((freenode "justinabrahms" ,znc-password)))))
    :config
    (setq erc-current-nick-highlight-type 'all)
    (setq erc-keywords '("jlilly"))
    ;; by default, erc alerts you on any activity. I only want to hear
    ;; about mentions of nick or keyword
    (setq erc-track-exclude-types '("JOIN" "PART" "NICK" "MODE" "QUIT"))
    (setq erc-track-use-faces t)
    (setq erc-track-faces-priority-list
          '(erc-current-nick-face erc-keyword-face))
    (setq erc-track-priority-faces-only 'all))

ibuffer

Having lots of buffers is a pretty common occurance in emacs, especially with a long-lived emacs process thanks to server-mode. As I'm writing this, I have 616 buffers open in emacs. Managing all that is difficult without some really helpful tools. ido-mode gets most of the way there as I can fuzzy find buffers based on their filename (and parent directories in the case of duplicates). For other times, I turn to ibuffer which presents a list of buffers. You can group these based on several parameters. I tend to do it based on project path or major mode.

(use-package ibuffer-vc
  :config
  (add-hook 'ibuffer-hook
    (lambda ()
      (ibuffer-vc-set-filter-groups-by-vc-root)
      (unless (eq ibuffer-sorting-mode 'alphabetic)
        (ibuffer-do-sort-by-alphabetic)))))
  

 ;;;###autoload
(defun ibuffer-set-filter-groups-by-path ()
  "Set the current filter groups to filter by file path."
  (interactive)
  (setq ibuffer-filter-groups
        (mapcar 'ibuffer-header-for-file-path
                (let ((paths (ibuffer-remove-duplicates
                              (mapcar 'buffer-file-name (buffer-list)))))
                  (if ibuffer-view-ibuffer
                      paths))))
  (ibuffer-update nil t))



(defun ibuffer-set-filter-groups-by-mode ()
  "Set the current filter groups to filter by mode."
  (interactive)
  (setq ibuffer-filter-groups
        (mapcar (lambda (mode)
                  (cons (format "%s" mode) `((mode . ,mode))))
                (let ((modes
                       (ibuffer-remove-duplicates
                        (mapcar (lambda (buf)
                                  (buffer-local-value 'major-mode buf))
                                (buffer-list)))))
                  (if ibuffer-view-ibuffer
                      modes
                    (delq 'ibuffer-mode modes)))))
  (ibuffer-update nil t))

Fancy Macros

(fset 'testify
   (lambda (&optional arg) "Converts test words into actual test functions.
  
  Converts something like `has token is 200` into `def
  test_has_token_is_200(self):\n\tpass` so I can easily type out my
  python test methods." (interactive "p") (kmacro-exec-ring-item (quote ([100 101 102 32 67108896 5 134217765 32 return 95 return 33 5 40 115 101 108 102 41 58 return 112 97 115 115 14 1 134217830 134217826] 0 "%d")) arg)))

  

Org-Mode

When tangling Makefiles, it's important that we preserve indentation of the resulting file, else we'll lose the tabs that make it a valid Makefile. One of these days, I'll bother to go hunting for a better build tool that's installed on every system always.

(require 'org)

(setq org-src-preserve-indentation t)

When using excorporate, I found that the agenda sometimes pulled in things it shouldn't. This will ensure that we don't parse datetimes wrong.

(require 'diary-lib)
(setq original-diary-time-regexp diary-time-regexp)
(setq diary-time-regexp (concat "\\b" diary-time-regexp))

I export things to xwiki sometimes. The mediawiki plugin mostly works, but links are wrong. This advice fixes it.

(defun xwiki-link-syntax (orig-fun &rest args)
  "Turns [link text] into [[text>>link]] to satisfy xwiki link formatting"
  (let ((res (apply orig-fun args)))
    (save-match-data
      (if (string-match "\\[\\(.*\\) \\(.*\\)\\]" res)
          (let ((link (match-string 1 res))
                (text (match-string 2 res)))
            (format "[[%s>>%s]]" text link))
        res))))

(advice-add 'org-mw-link :around #'xwiki-link-syntax)

I use org-agenda to keep an engineering log of my daily progress. This tells org-mode where to find the agenda file and set up a keyboard shortcut to invoke the agenda menu.

(setq org-directory (concat (getenv "HOME") "/Dropbox/docs/"))
(setq org-agenda-files (directory-files-recursively org-directory "org$"))

(define-key global-map "\C-ca" 'org-agenda)

This is a custom command based on Aaron Bieber's org post, which adds a custom agenda view.

(setq org-agenda-custom-commands
      '(("d" "Daily agenda and all TODOs"
         ((tags "PRIORITY=\"A\""
                ((org-agenda-skip-function '(org-agenda-skip-entry-if 'todo 'done))
                 (org-agenda-overriding-header "High-priority unfinished tasks:")))
          (alltodo ""
                   ((org-agenda-skip-function '(or (abrahms-org-skip-subtree-if-priority ?A)
                                                   (org-agenda-skip-entry-if 'todo '("WAITING"))
                                                   (org-agenda-skip-if nil '(scheduled deadline))))
                    (org-agenda-overriding-header "ALL normal priority tasks:")))
          (agenda "")
          (alltodo ""
                   ((org-agenda-skip-function '(org-agenda-skip-entry-if 'todo '("TODO")))
                    (org-agenda-overriding-header "Things I'm waiting on:"))))
         ((org-agenda-compact-blocks nil)))))

(defun abrahms-org-skip-subtree-if-priority (priority)
  "Skip an agenda subtree if it has a priority of PRIORITY.

PRIORITY may be one of the characters ?A, ?B, or ?C."
  (let ((subtree-end (save-excursion (org-end-of-subtree t)))
        (pri-value (* 1000 (- org-lowest-priority priority)))
        (pri-current (org-get-priority (thing-at-point 'line t))))
    (if (= pri-value pri-current)
        subtree-end
      nil)))

It's also interesting to play with org-super-agenda, which can generate filtered views into my org-file. Eventually, I hope to incorporate it with the custom agenda view above.

(use-package org-super-agenda
  :init (setq org-super-agenda-groups
              '(
                (:priority "A" :name "High priority")
                (:auto-property "WAITING_ON" :log t)

                ;; by project (more specific first)
                (:name "hygieia" :tag "hygieia")
                (:name "deploys" :tag "deploys")
                (:name "opex" :tag "opex")
                (:name "quality" :tag "quality")

                ;; normal priority
                (:name "Misc" :not (:priority "A"))))
  :config (org-super-agenda-mode 1))

My org file is formatted with a week numbers as headers. I can insert the headers with the command below.

(defun ja/org-insert-agenda-header ()
  "Inserts the week number at point"
  (interactive)
  (insert (format-time-string "week %V of %Y")))

(define-key org-mode-map "\C-cw" 'ja/org-insert-agenda-header)

I'd like it if graphviz dot files were auto-compile-able.

(org-babel-do-load-languages
 'org-babel-load-languages
 '((dot . t)
   (shell . t)
   (mscgen . t)))

Ditaa is a mechanism for turning ascii art into images. My emacsen doesn't ship with the jar it expects to have, so I install it with apt. This points it to the correct place.

(setq org-ditaa-jar-path "/usr/bin/ditaa")

Graphviz dotgraphs should use graphviz-dot-mode for syntax highlighting.

(add-to-list 'org-src-lang-modes '("dot" . graphviz-dot))
(unless (version<= emacs-version "26")
  (setq graphviz-dot-indent-width tab-width))

When compiling code (graphviz graphs, etc) with org-babel, I don't want to see prompts of "are you sure you want to do this?" for each code block, so we'll squelch them.

(setq org-confirm-babel-evaluate nil)

Additionally, when doing org-babel, I want any images to show up inline. This is useful for rapidly building mscgen or graphviz graphs.

(add-hook 'org-babel-after-execute-hook 'org-display-inline-images)

I've begun experimenting with capturing notes from various buffers via capture mode. Setup where those notes go.

(setq org-default-notes-file (concat org-directory "/captured.org"))
(global-set-key (kbd "C-c c") 'org-capture)

(setq org-capture-templates
      ;; Explaination of values here: 
      ;; https://orgmode.org/manual/Template-elements.html#Template-elements 
      `(("t" "Todo" entry (file+olp "~/Dropbox/docs/eng-log.org" "Engineering Worklog" ,(format-time-string "%Y (walmart)") ,(format-time-string "week %V of %Y")) "**** %?")
        ("m" "Meeting" entry (file+olp "~/Dropbox/docs/eng-log.org" "Engineering Worklog" ,(format-time-string "%Y (walmart)") ,(format-time-string "week %V of %Y")) "**** %?\n%t")
        ("i" "Item" entry (file+olp "~/Dropbox/docs/eng-log.org" "Engineering Worklog" ,(format-time-string "%Y (walmart)") ,(format-time-string "week %V of %Y")) "**** %?")
        ("a" "Action item" entry (file+olp "~/Dropbox/docs/eng-log.org" "Engineering Worklog" ,(format-time-string "%Y (walmart)") ,(format-time-string "week %V of %Y")) "* WAITING %?\n:PROPERTIES:\n:WAITING_ON: %^{Who owns this?}\n:END:\n %i %U %a")
        ))

Setup my org-mode keywords to include waiting & blocked:

  (setq org-todo-keywords
        '((sequence "TODO" "WAITING" "BLOCKED" "DONE")))

Ivy

Ivy is an improvement to emacs's extended-command-running functionality. It changes the menu to provide fuzzy match auto-completion stuff, which is speedier than helm and more compact.

(use-package ivy
  :config
  (setq ivy-count-format ""  ; Slim down ivy display
        ivy-display-style nil
        ivy-minibuffer-faces nil)
  ; Use Enter on a directory to navigate into the directory, not open it with dired.
  (define-key ivy-minibuffer-map (kbd "C-m") 'ivy-alt-done)
  (setq projectile-completion-system 'ivy) ; Let projectile use ivy
  
  (ivy-mode 1))


; Let ivy use flx for fuzzy-matching
;; (require 'flx)
;; (setq ivy-re-builders-alist '((t . ivy--regex-fuzzy)))



(use-package ivy-posframe
  :after ivy
  :diminish
  :config
  (setq ivy-posframe-display-functions-alist '((t . ivy-posframe-display-at-frame-top-center))
        ivy-posframe-height-alist '((t . 20))
        ivy-posframe-parameters '((internal-border-width . 10)))
  (setq ivy-posframe-width 70)
  (ivy-posframe-mode +1))

Email

So I'm trying out m4ue for work email, so I don't have to deal with exchange. This is powered through offlineimap.

OfflineImap

[general]
accounts = Walmart
maxsyncaccounts = 2
# helper python functions
pythonfile = ~/.offlineimap.py

After version 6.3.5, offlineimap also creates remote folders to match your local ones. This means we'll either need to reverse nametrans, or we'll need to just disable folder syncing back up to the parent. This seems fine for me, so let's just do that.

createfolders = False


[Account Walmart]
remoterepository = WalmartRemote
localrepository = WalmartLocal

[Repository WalmartRemote]
type = IMAP
sslcacertfile = /etc/ssl/certs/ca-certificates.crt
auth_mechanisms = GSSAPI, CRAM-MD5, LOGIN, XOAUTH2, PLAIN
ssl = yes
remotehosteval = get_param("walmart.remotehost")
remoteusereval = get_param("walmart.username")
remotepasseval = get_param("walmart.password")

# this will ensure that only the INBOX is synced
# folderfilter = lambda foldername: re.search('INBOX', foldername)
# It seems exchange's Calendar and Contacts can't be synced.
# For samples: https://offlineimap.readthedocs.org/en/latest/nametrans.html
folderfilter = lambda foldername: foldername not in ['Calendar', 'Contacts', '&XvqLrnaEgFR8+066-'] and 'Calendar' not in foldername

# If you don't want local changes to sync back to the server (e.g. deleting
# mail in the maildir will delete them on the server), uncomment this line:
#readonly = yes

[Repository WalmartLocal]
type = Maildir
localfolders = ~/Mail/Walmart

Fetch mail with offlineimap and rebuild mail database with mu index --rebuild --maildir=~/Mail.

I don't want to store my passwords in the plain text file for everyone to read, so I used GPG to encrypt them. This script will open that and decrypt it, which I pulled from this archwiki article. I'm storing my passwords in a python dict and pulling them out, so it's multi-account aware.

#! /usr/bin/env python2
from subprocess import check_output

def get_config(account):
        return eval(check_output("gpg -dq ~/.offlineimappass.gpg", shell=True).strip("\n"))[account]

def get_param(lookup):
        acct, attribute = lookup.split('.')
        return get_config(acct)[attribute]

Mu4e config

Mu4e isn't available via package.el, so we installed it from source.

(let ((mu4e-dir "/usr/local/share/emacs/site-lisp/mu4e/"))
  (setq mu4e-installed (file-exists-p mu4e-dir))
  (setq mu4e-compose-dont-reply-to-self t)
  (if mu4e-installed
      (progn
        (add-to-list 'load-path mu4e-dir)
        (require 'mu4e)
        (global-set-key (kbd "C-x m") 'mu4e) ;; setup a mail shortcut.
        (require 'org-mu4e)

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

        (require 'smtpmail)
        (setq mu4e-maildir "~/Mail")
        (setq mu4e-mu-binary "/usr/local/bin/mu")

        (custom-set-variables
         '(mu4e-view-mode-hook '(turn-on-visual-line-mode)))

        ;; enable viewing some messages in the browser if their html mode is useless.
        (add-to-list 'mu4e-view-actions
                     '("ViewInBrowser" . mu4e-action-view-in-browser) t)

        (ja/add-to-list-multiple 'mu4e-bookmarks
                                 '(("maildir:/Walmart/INBOX AND NOT flagged:trashed" "Walmart Inbox" ?i)
                                   ("maildir:/Walmart/github AND NOT flagged:trashed AND flag:unread" "Github" ?g)
                                   ("maildir:/Walmart/jira AND NOT flagged:trashed AND flag:unread" "Jira" ?j)
                                   ("maildir:/Walmart/L2-support AND NOT flagged:trashed AND flag:unread" "L2 Support" ?s)
                                   ))

        (setq user-full-name "Justin Abrahms")

        (setq mu4e-contexts `(
                              ,(make-mu4e-context
                                :name "Walmart"
                                ;; enter-func not defined
                                ;; leave-func not defined
                                :match-func (lambda (msg)
                                              (when msg
                                                (mu4e-message-contact-field-matches
                                                 msg :to ".*@walmartlabs.com")))
                                :vars '((user-mail-address . "justin.abrahms@walmartlabs.com")
                                        (mu4e-sent-folder . "/Walmart/Sent Items")
                                        (mu4e-drafts-folder . "/Walmart/Drafts")
                                        (mu4e-trash-folder . "/Walmart/Deleted Items")
                                        (mu4e-refile-folder . "/Walmart/Archive")
                                        (smtpmail-default-smtp-server . "smtp-gw1.wal-mart.com")
                                        (smtpmail-local-domain . "walmartlabs.com")
                                        (smtpmail-smtp-user . "t0a01lc")
                                        (smtpmail-smtp-server . "smtp-gw1.wal-mart.com")
                                        (smtpmail-stream-type . starttls)
                                        (smtpmail-smtp-service . 25)
                                        )
                                )

                              ))

        ;; press U to get updated email.
        (setq mu4e-get-mail-command "" ;; this is handled via cron
              send-mail-function 'smtpmail-send-it
              message-kill-buffer-on-exit t)

        ;; enable inline images
        (setq mu4e-view-show-images t)
        ;; use imagemagick, if available
        (when (fboundp 'imagemagick-register-types)
          (imagemagick-register-types))

        ;; for handling html emails
        (setq mu4e-html2text-command "w3m -T text/html")
        (setq mu4e-attachment-dir "~/Downloads/")

        ;; Prefer text, unless there's a 30x difference between plaintext and html length.
        (setq mu4e-view-html-plaintext-ratio-heuristic 30))))

Some emails are completely broken by the w3m handling above. Thankfully reddit user venkateshks came up with a bit of elisp which will open the message in chrome.

;;; message view action
(if mu4e-installed
    (progn
      (defun mu4e-msgv-action-view-in-browser (msg)
        "View the body of the message in a web browser."
        (interactive)
        (let ((html (mu4e-msg-field (mu4e-message-at-point t) :body-html))
              (tmpfile (format "%s/%d.html" temporary-file-directory (random))))
          (unless html
            (error "No html part for this message"))
          (with-temp-file tmpfile
            (insert "<html>" "<head><meta http-equiv=\"content-type\"" "content=\"text/html;charset=UTF-8\">" html))
          (browse-url (concat "file://" tmpfile))))
      (add-to-list 'mu4e-view-actions
                   '("View in browser" . mu4e-msgv-action-view-in-browser) t)))

ImapFilter

imapfilter is a tool which will run against your mail servers and move things around. From here, offlineimap will download them in the folders as they exist on the server. The configuration language is written in lua.

If you need to compile imapfilter on an ubuntu host, it looks like the stanza below. The LIBLUA overrides something in the Makefile because my system has liblua5.3.so not liblua.so.

LD_CONFIG=/usr/lib/x86_64-linux-gnu:$LD_CONFIG CPATH=/usr/include/lua5.3/:$INCLUDE_DIR make LIBLUA=-llua5.3 
local json = require ("dkjson")

-- This supposedly fixes the EOF violation of the protocol SSL error I
-- get. via https://github.com/lefcha/imapfilter/issues/22
options.timeout = 60;

function main()
   walmart()
end

function walmart()
   local walmart = IMAP {
      server = read_encrypted('walmart', 'remotehost'),
      username = read_encrypted('walmart', 'username'),
      password = read_encrypted('walmart', 'password'),
      ssl = 'ssl3',
   }
   walmart.INBOX:check_status()

   mails = walmart.INBOX:select_all()
   delete_mail_if_from(walmart, mails, 'sneal@')

   -- I don't want to hear how everyone accepts my calendar invites
   mails:contain_subject('Accepted: '):delete_messages()
   mails:contain_subject('Tenative: '):delete_messages()
   mails:contain_subject('Meeting Forward Notification: '):delete_messages()

   -- people filing PTO requests
   mails:contain_subject('Walmart Pay Team Calendar'):delete_messages()

   -- Don't tell me about mail purge jobs
   walmart['L2-support']:contain_subject('Nightly Purge'):delete_messages()

   eng_community = walmart.github:contain_subject('[engineering/community]')
   eng_community:contain_subject("] Release"):delete_messages()
end

function elemental()
   local elemental = IMAP {
      server = 'imap.gmail.com',
      username = read_encrypted('elemental', 'username'),
      password = read_encrypted('elemental', 'password'),
      ssl = 'tls1.2',
   }

   elemental.INBOX:check_status()
   elemental['[Gmail]/Trash']:check_status()
   elemental['[Gmail]/Spam']:check_status()
   mails = elemental.INBOX:select_all() + elemental['[Gmail]/Spam']

   delete_mail_if_from(elemental, mails, 'cloud_notifications_dev@elementaltechnologies.com')
   delete_mail_if_subject_contains(elemental, mails, '[confluence]')
   delete_mail_if_subject_contains(elemental, mails, 'Zuora')

end

function amazon()
   local amazon = IMAP{
      server = read_encrypted('amazon', 'remotehost'),
      port = tonumber(read_encrypted('amazon', 'remoteport')),
      username = read_encrypted('amazon', 'username'),
      password = read_encrypted('amazon', 'password'),
      ssl = 'ssl3',
   }
   amazon.INBOX:check_status()
   amazon['INBOX/codex']:check_status()

   mails = amazon.INBOX:select_all()

   -- codex configs
   codex = amazon['INBOX/codex']:select_all()
      + mails:contain_to('codex-dev@amazon.com')
      + mails:contain_to("codex@amazon.com")
      + mails:contain_to('code-review-admins@amazon.com')
   delete_mail_if_from(amazon, codex, 'root@amazon.com')
   delete_mail_if_from(amazon, codex, 'issues@amazon.com')
   delete_mail_if_subject_contains(amazon, codex, "[PackageBuilder]")
   delete_mail_if_subject_contains(amazon, codex, "[CD]")
   delete_mail_if_subject_contains(amazon, codex, "[ALERT]")
end

function delete_mail_if_subject_contains(account, mails, subject)
    filtered = mails:contain_subject(subject)
    filtered:delete_messages()
end

function delete_mail_if_from(account, mails, from)
   filtered = mails:contain_from(from)
   filtered:delete_messages()
end

-- Utility function to get IMAP password from a gpg encrypted file
function read_encrypted(key, value)
   local handle = io.popen("gpg -dq ~/.offlineimappass.gpg")
   local result = handle:read("*a")
   handle:close()
   local obj, pos, err = json.decode (result, 1, nil)
   if err then
      print("Unable to parse json. ERR: " + err)
      return ""
   end
   return obj[key][value]
end


main() -- execute main function from above

We also want to setup a crontab to fetch our mail every so often.

*/5 * * * * eval `luarocks path` && imapfilter && offlineimap

Handle mailto links

Thanks to this emacswiki entry, this script will open emacs and compose an email.

emacsclient -c --eval "(browse-url-mail \"$@\")"  

Calendaring

I'd like to get calendaring setup so that my exchange calendar dumps into an org-mode file. I suspect this will make my agenda mode easier to use and more accurate.

To use it, open up the calendar (M-x calendar) and press e to update.

(use-package excorporate)

(setq-default
 ;; configure email address and office 365 exchange server adddress for exchange web services
 excorporate-configuration
 (quote
  ("t0a01lc@homeoffice.wal-mart.com" . "https://outlook.wal-mart.com/EWS/Exchange.asmx"))
 ;; integrate emacs diary entries into org agenda
 org-agenda-include-diary t
 )

Host specific overrides

For some hosts (such as my work machine), I want to change up the config a little. That happens here.

(load-if-exists (concat "~/" (system-name) ".el"))

Miscellaneous stuff

Dedicated Mode

Dedicated mode fixes the issue in which emacs spawns a new window (for tab completion or help, for instance) and it replaces an existing buffer you had open which you wanted to be persistent. If you turn on the dedicated minor-mode, none of those transient buffers will open up over those buffers.

(require 'dedicated) ;; sticky windows

Fill Column Indicator

Fill column indicator will show you the current fill-column as a vertical line in your buffers. This is helpful for making sure your code doesn't go over 80 characters wide for things like python.

(require 'fill-column-indicator) ;; line indicating some edge column

scpaste

SCPaste is sort of like gists, but it uploads the paste to your own server. It was particularly helpful when dealing with things at Google when I couldn't post it publically (or even privately to an external service). One of the neat things it does is it uses your color scheme (if you use a colored emacs) in the paste.

  ;; scpaste
  (setq scpaste-http-destination "http://justin.abrah.ms/pastes"
        scpaste-scp-destination "justin.abrah.ms:/srv/justin.abrah.ms/pastes")

Keybindings

Just a few custom keybindings I have. The big ones here are my window moving commands. The emacs default is C-x o which will progress through the windows in some semi-sane order one at a time. What I find myself actually wanting is something akin to vim movement commands. The unfortunate situation is that the key-bindings I'm using aren't in the space of keybindings reserved for users to override. This has the unfortunate side effect of meaning that I need to override it in a half a dozen different modes. I'm still looking for a better solution. I think it might be to use the super key which is still reserved but less likely to be used.

;; Vim style keyboard moving
(global-set-key (kbd "C-M-l") 'windmove-right)
(global-set-key (kbd "C-M-h") 'windmove-left)
(global-set-key (kbd "C-M-j") 'windmove-down)
(global-set-key (kbd "C-M-k") 'windmove-up)
(global-set-key (kbd "M-o") 'other-window)
(global-set-key (kbd "C-c g") 'recompile)
(global-unset-key (kbd "C-z")) ; suspending frame is useless with emacsclient and/or tmux
(add-hook 'perl-mode-hook (lambda ()
                            (local-set-key (kbd "C-M-h") 'windmove-left)))
(add-hook 'ruby-mode-hook (lambda ()
                            (local-set-key (kbd "C-M-h") 'windmove-left)))
(add-hook 'c-mode-common-hook (lambda ()
                                (local-set-key (kbd "C-M-h") 'windmove-left)))

(global-set-key (kbd "M-j") (lambda ()
                              (interactive)
                              (join-line -1)))

Platform Hacks

Using Emacs from within the terminal in OSX completely breaks copy+paste support. This chunk of code from emacswiki restores it.

  (defun copy-from-osx ()
    (shell-command-to-string "pbpaste"))
  
  (defun paste-to-osx (text &optional push)
    (let ((process-connection-type nil))
      (let ((proc (start-process "pbcopy" "*Messages*" "pbcopy")))
        (process-send-string proc text)
        (process-send-eof proc))))
  (if (eq system-type 'darwin)
      (progn
        (setq interprogram-cut-function 'paste-to-osx)
        (setq interprogram-paste-function 'copy-from-osx)))

Beancounter

I track my finances using Martin Blais's beancount program. It's a little neckbeardy, but I like the flexibility it offers and the option to write small python scripts against my finances. Because it runs on python3, my virtualenv setup isn't quite there to support it. As such, we'll manually set the path to the bean-check file which does the linting of my balance sheets.

(setq beancount-check-program (expand-file-name "~/.virtualenvs3/beancount/bin/bean-check"))

Snippets

It's quite nice to have tab completing expansion similar to textmate snippets. There's a thing that does this in emacs called yasnippets.

(require 'yasnippet)
(yas-global-mode 1)

(if (file-exists-p (expand-file-name "~/src/github.com/dominikh/yasnippet-go"))
    (add-to-list 'yas-snippet-dirs (expand-file-name "~/src/github.com/dominikh/yasnippet-go/")))


We can set up some snippets that are based on specific modes.

# key: pdb
# name: Debugging Statement
# contributor: Justin Abrahms <justin@abrah.ms>
# --
import pdb; pdb.set_trace()

Magit

Magit requires emacs 24.4. We'll validate the version is at least that before installing it.

From this Stack Overflow question, this snippet of code advises makes it so magit commits always show the verbose commit stuff when you're writing your commit message. This is helpful to me because I often forget what it is that I'm committing and need a simple refresher.

(if (emacs-version-gt-p 24 4)
    (use-package magit
      :init
      (advice-add #'magit-key-mode-popup-committing :after
                (lambda ()
                  (magit-key-mode-toggle-option (quote committing) "--verbose")))))

There's a comprehensive approach to getting access to pull requests and such from within emacs called "Forge". This sets that up to load after magit.

(if (emacs-version-gt-p 24 4)
    (use-package forge
      :after magit
      :config
      (add-to-list 'forge-alist '("gecgithub01.walmart.com" "gecgithub01.walmart.com/api/v3" "walmart" forge-github-repository))))

It's also quite nice to be able to open the current point on your repo host. This package solves that.

(use-package browse-at-remote
             :config
             (add-to-list 'browse-at-remote-remote-type-domains '("gecgithub01.walmart.com" . "github")))

gc stops

According to flx (the matcher for flx-ido), GC triggers every 0.76MB by default. Given we're on a modern machine, we can up it to a still-conservative 100MB.

(setq gc-cons-threshold 100000000)

Projectile

Projectile lets you move around a project, wherein project is defined as something like "git repository".

(projectile-global-mode)
(setq projectile-enable-caching t)

deadgrep

Deadgrep is a tool which makes searching easier and faster. It uses ripgrep under the hood, which ignores things like .git directories and obeys your .gitignore. It's like ack or ag, but faster.

Because old habits die hard, alias find-grep to deadgrep.

(use-package deadgrep
  :ensure-system-package (rg . "cd /tmp && curl -LO https://github.com/BurntSushi/ripgrep/releases/download/11.0.2/ripgrep_11.0.2_amd64.deb && sudo dpkg -i ripgrep_11.0.2_amd64.deb"))

(defalias 'find-grep 'deadgrep)

pepita

We use splunk at work. Pepita is a small library that allows me to run splunk queries and get the results back to emacs. It seems pretty interesting.

(use-package pepita)

Flycheck

Flycheck does syntax checking for your buffer. We install it with use-package.

(use-package flycheck
 :hook (sh-mode go-mode python-mode js2-mode))

Themes

Doom-themes are a modern set of themes for emacs.

(use-package doom-themes
  :config
  ;; Global settings (defaults)
  (setq doom-themes-enable-bold t    ; if nil, bold is universally disabled
        doom-themes-enable-italic t) ; if nil, italics is universally disabled

  ;; Enable flashing mode-line on errors
  (doom-themes-visual-bell-config)
  
  ;; Enable custom neotree theme (all-the-icons must be installed!)
  (doom-themes-neotree-config)
  ;; or for treemacs users
  (setq doom-themes-treemacs-theme "doom-colors") ; use the colorful treemacs theme
  (doom-themes-treemacs-config)
  
  ;; Corrects (and improves) org-mode's native fontification.
  (doom-themes-org-config))

I want my themes to be dark at night and light in the morning. Thankfully, there's an emacs mode for that.

;; Install additinal themes from melpa
;; make sure to use :defer keyword
;; (use-package ir-black-theme :defer)
;; (use-package material-theme :defer)

(use-package circadian
  :config
  (setq calendar-latitude 45.5)
  (setq calendar-longitude -122.6)
  (setq circadian-themes '((:sunrise . doom-molokai)
                           (:sunset  . doom-peacock)))
  (circadian-setup))

PDF viewing

(use-package pdf-tools
  ;; Unsure how to make this depend on libraries, but should get an
  ;; answer at:
  ;; https://github.com/waymondo/use-package-ensure-system-package/issues/2
  ;; libpng-dev zlib1g-dev libpoppler-glib-dev libpoppler-private-dev imagemagick
  )

Manual setup

Having Unity intercept my superkey is less than ideal. unity-tweak-tool makes that work for me. This comment on stack exchange says I have to click Unity > Additional. From there, I uncheck the "Hold Super for keyboard shortcuts" option, disable "Invoke HUD" (clicking they keybinding and pressing backspace), then make the "Show the Launcher" option <Super>space.

Undocumented

These are things, for whatever reason, I haven't had a chance to document. Some of it, I forgot why I added it, but assume it was for a reason (I already feel ashamed. Let's not talk about it.) Others are temporary. The rest are so small, I didn't have much to say about them.

(ac-config-default)

(global-visual-fill-column-mode) ;; always enable visual-fill-mode


;;; set the trigger key so that it can work together with yasnippet on tab key,
;;; if the word exists in yasnippet, pressing tab will cause yasnippet to
;;; activate, otherwise, auto-complete will
(ac-set-trigger-key "TAB")
(ac-set-trigger-key "<tab>")

(require 'auto-complete-config)
  
  (defun create-tags (dir-name)
    "Create tags file."
    (interactive "DDirectory: ")
    (shell-command
     (format "find %s -type f | xargs %s -a -o %s/TAGS" dir-name path-to-etags dir-name)))
  
  (setq auto-mode-alist ;; files called .bashrc should be opened in sh-mode
        (append
         '(("\\.bashrc" . sh-mode))
         '(("Vagrantfile" . ruby-mode))
         auto-mode-alist))
  
  ;; tempfiles, stolen from github://defunkt/emacs
  (defvar user-temporary-file-directory
    (concat temporary-file-directory user-login-name "/"))
  (make-directory user-temporary-file-directory t)
  (setq backup-by-copying t
        backup-directory-alist `(("." . ,user-temporary-file-directory))
        auto-save-list-file-prefix (concat user-temporary-file-directory ".auto-saves-")
        auto-save-file-name-transforms `((".*" ,user-temporary-file-directory)))
  
  
  ;;; hooks
  (require 'dired-x)
  (add-hook 'dired-mode-hook (lambda ()
                               (dired-omit-mode 1)))
  
  ;; Fantastic dired thing which will hide most `ls -l` output. 
  ;; ) and ( to toggle it
  ;; (require 'dired-details)
  ;; (dired-details-install)
  
  ;; scala
  (let ((ensime-load-path "~/src/ensime/elisp/")
        (sbt-bin "~/bin/")
        (scala-bin "~/src/scala-2.9.2/bin/"))
    (if (file-exists-p ensime-load-path)
        (progn
          (add-to-list 'load-path ensime-load-path)
          (require 'scala-mode)
          (require 'ensime)
          (push scala-bin exec-path)
          (push sbt-bin exec-path)
          (add-to-list 'auto-mode-alist '("\\.scala$" . scala-mode))
          (add-hook 'scala-mode-hook '(lambda ()
                                        (scala-mode-feature-electric-mode)
                                        ))
  
          
          (add-hook 'scala-mode-hook 'ensime-scala-mode-hook))))
  
  ;; minibuffer command history
  (setq savehist-additional-variables    ;; also save...
    '(search-ring regexp-search-ring)    ;; ... my search entries
    savehist-file "~/.emacs.d/savehist") ;; keep my home clean
  (savehist-mode t)                      ;; do customization before activate
  
  (defun jump-to-next-char (c &optional count)
    "Jump forward or backward to a specific character.  With a
  count, move that many copies of the character."
    (interactive "cchar: \np")
    (when (string= (string c) (buffer-substring (point) (+ 1 (point))))
      (setq count (+ 1 count)))
    (and
     (search-forward (string c) nil t count)
     (> count 0)
     (backward-char)))
  (global-set-key (kbd "C-:") 'jump-to-next-char)
  
  (setq compilation-scroll-output 'first-error)
  
  ;; turning on autofill everywhere seems to give errors like "error in
  ;; process filter: Wrong type argument: stringp, nil" and other randomness.
  (remove-hook 'text-mode-hook 'turn-on-auto-fill)
  
  (put 'upcase-region 'disabled nil)
  (put 'downcase-region 'disabled nil)
  (put 'set-goal-column 'disabled nil)
  (put 'narrow-to-region 'disabled nil)
  
  (custom-set-variables
   ;; custom-set-variables was added by Custom.
   ;; If you edit it by hand, you could mess it up, so be careful.
   ;; Your init file should contain only one such instance.
   ;; If there is more than one, they won't work right.
   '(custom-safe-themes (quote ("870a63a25a2756074e53e5ee28f3f890332ddc21f9e87d583c5387285e882099" "159bb8f86836ea30261ece64ac695dc490e871d57107016c09f286146f0dae64" "5e1d1564b6a2435a2054aa345e81c89539a72c4cad8536cfe02583e0b7d5e2fa" "211bb9b24001d066a646809727efb9c9a2665c270c753aa125bace5e899cb523" "5727ad01be0a0d371f6e26c72f2ef2bafdc483063de26c88eaceea0674deb3d9" "30fe7e72186c728bd7c3e1b8d67bc10b846119c45a0f35c972ed427c45bacc19" default)))
   '(display-time-mode t)
   '(elisp-cache-byte-compile-files nil)
   '(erc-truncate-mode t)
   '(google-imports-file-for-tag (quote (("ServiceException" . "javax.xml.rpc.ServiceException") ("MalformedURLException" . "java.net.MalformedURLException") ("URL" . "java.net.URL") ("Named" . "com.google.inject.name.Named") ("Inject" . "com.google.inject.Inject") ("FormattingLogger" . "java/com/google/common/logging/FormattingLogger.java"))))
   '(menu-bar-mode nil)
   '(minibuffer-prompt-properties (quote (read-only t point-entered minibuffer-avoid-prompt face minibuffer-prompt)))
   '(safe-local-variable-values (quote ((virtualenv-default-directory . "/Users/justinlilly/src/prbot/") (virtualenv-workon . "prbot") (Mode . js))))
   '(tool-bar-mode nil))