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

sudo add-apt-repository -y ppa:justinabrahms/ttf-cascadia-code
sudo apt-get install -y ttf-anonymous-pro xclip autojump texlive texlive-latex-extra git w3m fonts-inconsolata xfonts-terminus ttf-cascadia-code
sudo apt-get install -y aptitude lua5.3 luarocks unity-tweak-tool luarocks texinfo tree offlineimap imapfilter ttf-ancient-fonts unifont gnuplot-x11
sudo apt-get install -y shellcheck
sudo apt-get install -y exuberant-ctags htop
sudo apt-get install -y fonts-hack-ttf  # alternatively https://github.com/chrissimpkins/Hack/

# This may require changing the mirror with the --only-server flag.
luarocks --local install dkjson

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-2.6-dev libxapian-dev libgtk-3-0 libwebkitgtk-3.0-dev
cd /tmp
wget https://github.com/djcb/mu/archive/v0.9.16.zip
extract v0.9.16.zip
cd mu-0.9.16/
autoreconf -i
./configure
make
sudo make install

TODO s

  • persistent eshell history (maybe it pulls in normal shell history too)
  • 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
                            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
                            helm ;; fuzzy finding for M-x & buffers
                            graphviz-dot-mode ;; graphviz syntax highlighting
                            ag ;; emacs frontend to ag (replacement for find-grep)
                            ivy ;; makes swapping buffers and such show better autocomplete
                            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))

I'm going to start 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))

Let's also keep our packages up to date, which will probably have No Adverse Affects (tm)

(use-package auto-package-update
  :ensure
  :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 some promise!

(use-package use-package-ensure-system-package
  :ensure t)

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
  :ensure)
(exec-path-from-shell-initialize)

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

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

(if (emacs-version-gt-p 24 4)
    (progn
      (add-to-list 'my-packages 'magit)
      (add-to-list 'my-packages 'forge)))

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

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

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) 

I like to use google-chrome, because it has a fast javascript engine and my settings sync across different computers. This ensures when I open links with M-x browse-url that it opens in chrome.

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

I want the title of my emacs to indicate what I'm working on. This is so that thyme can pick up on it.

(setq frame-title-format
      '((:eval (if (buffer-file-name)
                   (abbreviate-file-name (buffer-file-name))
                 "%b"))
        (:eval (if (buffer-modified-p)
                   " •"))
        " - Emacs")
    )

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)
                                (flycheck-mode 1)
                                (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*.

  (if (file-exists-p "~/.shell/variables")
      ;; TODO: load $PATH from that file.
      ;; TODO: Add $PATH to exec-path
      nil)
  
  ;;; Necessary to make some modes aware of binaries, such as sql-mysql
(setq exec-path (split-string (getenv "PATH") ":"))
  
(setenv "PATH" (concat (getenv "PATH") ":" "/usr/local/bin"))
(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"))

Emacs seems to have difficulty getting the proper PATH variable set. I managed to find this snippet at http://emacswiki.org/emacs/EmacsApp which seems to fix things.

  (if (not (getenv "TERM_PROGRAM"))
      (let ((path (shell-command-to-string
                 "source $HOME/.shell/variables && printf %s \"\$PATH\"")))
        (setenv "PATH" path)
        (setq exec-path (split-string path ":"))))
  

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"

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
  :ensure
  :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 js2-refactor
  :requires js2-mode)

(use-package prettier-js
  :ensure
  :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 'flycheck-mode)
(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, if it exists, via https://emacs.cafe/emacs/javascript/setup/2017/05/09/emacs-setup-javascript-2.html
(if (executable-find "tern")
    (progn
      (require 'company)
      (require 'company-tern)

      (add-to-list 'company-backends 'company-tern)
      (add-hook 'js2-mode-hook (lambda ()
                                 (tern-mode)
                                 (company-mode)))

      ;; Disable completion keybindings, as we use xref-js2 instead
      (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))
  (message "Skipping js autocomplete b/c tern isn't installed. Consider installing it with `npm install -g tern`"))


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

To setup javascript support within ctags using romainl/ctags-patterns-for-javascript, we need to add the following text file to `~/.ctags` That file doesn't understand environment variables like $HOME, so we need to manually configure it.

--options=$HOME/src/github.com/romainl/ctags-patterns-for-javascript/ctagsrc

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
  :ensure
  :demand)

(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
  :ensure
  :requires js2-mode)

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
  :ensure
  :hook (js2-mode . lsp)
  :commands lsp lsp-deferred
  :ensure-system-package
  ((typescript-language-server . "npm install -g typescript-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 helm-lsp
  ;; type completion for xref-aprops
  :commands helm-lsp-workspace-symbol)

(use-package dap-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"))

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

Shell mode

Shell scripts are notoriously finnicky and prone to bugs due to weird bashisms about quoting variables and such. shellcheck, a linter, can make that easier.

(add-hook 'sh-mode-hook 'flycheck-mode)

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

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
  :hook go-mode)

;; (use-package go-autocomplete)
(use-package gotest)

;; TODO(abrahms): maybe gopath isn't setup??
(use-package golint
  :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
  :init
  ;; setting up autocomplete should happen after yasnippet so we don't duplciate tab bindings.
  (require 'auto-complete-config))

(use-package 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"))
  (flycheck-mode)
  (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
    :ensure
    :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))

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)
    :ensure
    :init
    (setq znc-servers
          `(("justin.abrah.ms" 5000 t
             ((freenode "justinabrahms" ,znc-password)
              (synirc "justinabrahms/synirc" ,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.

;; ibuffer configs
(setq ibuffer-saved-filter-groups
      '(("default"
         ("sprintly-main" (filename . "/src/sprintly/sprint.ly/snowbird/"))
         ("sprintly-js" (filename . "/src/sprintly/sprint.ly/html/"))
         ("sprintly-misc" (filename . "/src/sprintly/sprint.ly/"))
         ("sprintly-chef" (filename . "/src/sprintly/sprint.ly-chef/"))
         ("holiday-extras" (filename . "/src/holiday_extras/"))
         ("glider" (filename . "/src/glider/"))
         ("amazon" (filename . "/src/amazon/"))
         ("gitstreams" (filename . "/src/gitstreams/"))
         ("org" (mode . org-mode))
         ("irc" (mode . erc-mode))
         ("background" (name . "^*.**$")))))


(defun ibuffer-header-for-file-path (file-path)
  (if-let* ((prefix (concat (getenv "HOME") "/src/amazon/"))
         (is-amazon (string-prefix-p prefix file-path))
         (versionset-offset (count "/" (split-string prefix "") :test 'string=))
         (package-offset (+ 2 versionset-offset))
         (versionset (nth versionset-offset (split-string file-path "/")))
         (package (nth package-offset (split-string file-path "/"))))
      (concat "ws:" versionset " pkg:" package)
    "Default"
    ))

; (ibuffer-header-for-file-path "/home/local/ANT/abrahmsj/src/amazon/so-jelly/src/ElementalJellyfishService/Config")
;; "ws:so-jelly pkg:ElementalJellyfishService"
; (ibuffer-header-for-file-path "/home/local/ANT/abrahmsj/src/github.com/GloriaAnholt/image-gallery/LAB.md")
;; Default
  


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

This is a WIP script to generate dynamic ibuffer headers based on the file paths. My goal is to have these headers be auto-defined based on conventional file locations. $HOME/src/github.com/justinabrahms/dotfiles should map to a header called justinabrahms/dotfiles, for instance.

(defun ibuffer-header-for-file-path (file-path)
  (if-let* ((prefix (concat (getenv "HOME") "/src/amazon/"))
         (is-amazon (string-prefix-p prefix file-path))
         (versionset-offset (count "/" (split-string prefix "") :test 'string=))
         (package-offset (+ 2 versionset-offset))
         (versionset (nth versionset-offset (split-string file-path "/")))
         (package (nth package-offset (split-string file-path "/"))))
      (concat "ws:" versionset " pkg:" package)
    "Default"
    ))

; (ibuffer-header-for-file-path "/home/local/ANT/abrahmsj/src/amazon/so-jelly/src/ElementalJellyfishService/Config")
;; "ws:so-jelly pkg:ElementalJellyfishService"
; (ibuffer-header-for-file-path "/home/local/ANT/abrahmsj/src/github.com/GloriaAnholt/image-gallery/LAB.md")
;; Default
  
  (setq ibuffer-show-empty-filter-groups nil)
  (add-hook 'ibuffer-mode-hook ; refresh buffer groups on ibuffer mode.
            (lambda ()
              (ibuffer-auto-mode 1)
              (ibuffer-switch-to-saved-filter-groups "default")))

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)

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'm experimenting with org-agenda for keeping an engineering log of my daily progress. Let's tell 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)))

My org file is formatted with a week number and a list of days. I can insert the headers with the command below, and I insert each day with C-c . as the header of that day.

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

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.

(require 'ivy)
(ivy-mode 1)

; Slim down ivy display
(setq ivy-count-format ""
      ivy-display-style nil
      ivy-minibuffer-faces nil)

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

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

; Let projectile use ivy
(setq projectile-completion-system 'ivy)

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 = Amazon
maxsyncaccounts = 2
# helper python functions
pythonfile = ~/.offlineimap.py

[Account Elemental]
remoterepository = ElementalRemote
localrepository = ElementalLocal
synclabels = yes

[Repository ElementalLocal]
type = Maildir
localfolders = ~/Mail/Elemental

[Repository ElementalRemote]
type = Gmail
remoteusereval = get_config("elemental")["username"]
remotepasseval = get_config("elemental")["password"]
sslcacertfile = /etc/ssl/certs/ca-certificates.crt
folderfilter = lambda foldername: foldername not in ('[Gmail]/All Mail', '[Gmail]/Important')
nametrans = lambda foldername: re.sub ('sent_mail', 'sent',
                               re.sub ('starred', 'flagged',
                               re.sub (' ', '_', foldername.lower())))

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 Amazon]
remoterepository = AmazonRemote
localrepository = AmazonLocal

[Repository AmazonRemote]
type = IMAP
# If this is on Ubuntu, uncomment the following sslcacertfile line.  You also
# need to install the "amazon-pidgin" package to get this CA cert.
sslcacertfile = /usr/share/purple/ca-certs/Amazon.com Internal Root Certificate Authority.pem
auth_mechanisms = GSSAPI, CRAM-MD5, LOGIN, XOAUTH2, PLAIN
ssl = yes
remotehosteval = get_param("amazon.remotehost")
remoteport = 1993
remoteusereval = get_param("amazon.username")
remotepasseval = get_param("amazon.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-']

# 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 AmazonLocal]
type = Maildir
localfolders = ~/Mail/Amazon

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 '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:/Elemental/INBOX AND NOT flagged:trashed" "Elemental Inbox" ?e)
                                   ("maildir:/Amazon/INBOX AND NOT flagged:trashed" "Amazon Inbox" ?a)))

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

        (setq mu4e-contexts `(
                              ,(make-mu4e-context
                                :name "Amazon"
                                ;; enter-func not defined
                                ;; leave-func not defined
                                :match-func (lambda (msg)
                                              (when msg
                                                (mu4e-message-contact-field-matches
                                                 msg :to ".*@amazon.com")))
                                :vars '((user-mail-address . "abrahmsj@amazon.com")
                                        (mu4e-sent-folder . "/Amazon/Sent Items")
                                        (mu4e-drafts-folder . "/Amazon/Drafts")
                                        (mu4e-trash-folder . "/Amazon/Deleted Items")
                                        (smtpmail-default-smtp-server . "ballard.amazon.com")
                                        (smtpmail-local-domain . "amazon.com")
                                        (smtpmail-smtp-user . "ANT\\abrahmsj")
                                        (smtpmail-smtp-server . "ballard.amazon.com")
                                        (smtpmail-stream-type . starttls)
                                        (smtpmail-smtp-service . 1587)
                                        )
                                )

                              ,(make-mu4e-context
                                :name "Elemental"
                                :match-func (lambda (msg)
                                              (when msg
                                                (mu4e-message-contact-field-matches msg
                                                                                    :to ".*@elementaltechnologies.com")))
                                :vars '((user-mail-address . "abrahmsj@elementaltechnologies.com")
                                        (mu4e-sent-folder . "/Elemental/[gmail].sent")
                                        (mu4e-drafts-folder . "/Elemental/[gmail].drafts")
                                        (mu4e-trash-folder . "/Elemental/[gmail].trash")
                                        ;; These sendmail settings don't work yet.
                                        ;; emacs24 versions
                                        (smtpmail-stream-type . starttls)
                                        (smtpmail-default-smtp-server . "smtp.gmail.com")
                                        (smtpmail-smtp-server . "smtp.gmail.com")
                                        (smtpmail-smtp-service . 587)

                                        ))
                              ))

        ;; press U to get updated email.
        (setq mu4e-get-mail-command "imapfilter; offlineimap"
              send-mail-function 'smtpmail-send-it
              message-kill-buffer-on-exit t)

        ;; 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()
--   elemental()
   amazon()
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

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

(setq-default
 ;; configure email address and office 365 exchange server adddress for exchange web services
 excorporate-configuration
 (quote
  ("justin.abrahms@walmart.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

encryption mode

I keep a file around of encrypted passwords that emacs needs to know about (simple stuff like IRC server password). 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")

Flymake

Navigating around flymake bits is a bit of a pain. This makes things a little easier.

(defun abrahms-flymake-show-error (prefix)
  (interactive "p")
  (if prefix
      (flymake-goto-next-error)
    (flymake-goto-prev-error))
  (flymake-display-err-menu-for-current-line))

(global-set-key "\C-c\C-v" 'abrahms-flymake-show-error)

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-x m")) ; I don't use mail
(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)

(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

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 (memq 'magit my-packages)
    (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.

(use-package forge
  :ensure
  :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
             :ensure
             :config
             (add-to-list 'browse-at-remote-remote-type-domains '("gecgithub01.walmart.com" . "github")))

helm mode

I've swapped out of using ido mode and instead use helm mode. Use that for buffer switching and M-x.

(require 'helm)
(helm-mode 1)
(global-set-key (kbd "M-x") 'helm-M-x)
(global-set-key (kbd "C-x b") 'helm-buffers-list)

Searching for files

ag is an awesome tool to search for things. Like grep, but it's aware of all the dev stuff like not to traverse .git folders.

(use-package ag
  :ensure-system-package (ag . silversearcher-ag))

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)

ag

Ag is a tool which makes searching easier and faster. It ignores things like .git directories and obeys your .gitignore. It's like ack, but 10x faster.

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

(defalias 'find-grep 'ag)

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

Flycheck

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

(use-package flycheck
  :ensure)

Themes

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 :ensure :defer)
(use-package material-theme :ensure :defer)

(use-package circadian
  :ensure t
  :config
  (setq calendar-latitude 45.516022)
  (setq calendar-longitude -122.681427)
  (setq circadian-themes '((:sunrise . material-light)
                           (:sunset  . ir-black)))
  (circadian-setup))

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))
  (custom-set-faces
   ;; custom-set-faces 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.
   '(mode-line-inactive ((t (:inherit mode-line :background "color-20" :foreground "white" :box (:line-width -1 :color "grey40") :weight light)))))