My Emacs Configuration

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.

There's a lot of prompting of shell commands that happens as part of the initial launch. This disables that notification and just opens the windows that are necessary.

(setq async-shell-command-buffer 'new-buffer) ;; don't prompt for processes opening buffers.

While I've made some effort to get system-packages working for initial setup, it's incomplete.

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

snap install fx

Or on windows:

winget install --exact golang.go
# Set your PATH to include C:\go\bin

winget install --exact git.git
# Set your path to include C:\Program Files\Git\bin

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 hub libtool autoconf libgmime-3.0-dev postfix libxapian-dev libgtk-3-0 libwebkitgtk-3.0-dev guile-2.0-dev html2text xdg-utils
cd `mktemp -d`
ghget djcb/mu
autoreconf -i
./configure
make
sudo make install

TODOs

  • implement ffap for python.
  • sql format tool

Look into these packages

Initial setup

Increase the garbage collection threshold so we don't spend a bunch of time in startup doing GC. We'll turn it back lower (so we do small quick ones) after we're done loading.

;; The default is 800 kilobytes.  Measured in bytes.
;; without: *** Emacs loaded in 21.20 seconds with 55 garbage collections.
;; with   : *** Emacs loaded in 19.34 seconds with 18 garbage collections.
(setq gc-cons-threshold (* 100 1000 1000))


;; Profile emacs startup
(add-hook 'emacs-startup-hook
          (lambda ()
            (message "*** Emacs loaded in %s with %d garbage collections."
                     (format "%.2f seconds"
                             (float-time
                              (time-subtract after-init-time before-init-time)))
                     gcs-done)))

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
             '("nongnu" . "http://elpa.nongnu.org/nongnu/") t)

(package-initialize)

Get an up-to-date list of packages available to us if a cached copy doesn't exist.

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

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

(if (equal window-system 'w32)
    (push "/Program Files/Git/bin" exec-path))

(if (equal window-system 'x)
    (push "/snap/bin" exec-path))

Install use-package and set it up.

(when (not (package-installed-p 'use-package))
    (ignore-errors (package-install '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. We need to provide a shim if we're using it in windows though.

(if (not (equal window-system 'w32))
    (use-package use-package-ensure-system-package)
  (progn
    (add-to-list 'use-package-keywords :ensure-system-package)
    (defun use-package-normalize/:ensure-system-package (name-symbol keywords args))
    (defun use-package-handler/:ensure-system-package (name-symbol keyword archive-name rest state))))

For some reason, osx gets re-set to apt as the package manager, so we have to force it into using brew.

(if (eq system-type 'darwin)
    (progn
      (setq system-packages-package-manager 'brew
            system-packages-use-sudo nil)))

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

(if (not (equal window-system 'w32))
    (progn
      (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.

(if (not (equal window-system 'w32))
  (use-package system-packages))


(setq packages-to-install (cond ((eq system-type 'darwin) '("node" "autojump" "golang" "git-delta" "docker" "docker-compose" "cmake" "hub" "graphviz"
                                                            "gpg" "mactex"
                                                            "libtool" ; use for vterm
                                                            ))
                                ((eq system-type 'gnu/linux)
                                     (mapcar 'symbol-name '(
                                                            xclip ; clipboard tooling
                                       autojump ; quickly navigate directories in terminal
                                       texlive texlive-latex-extra texinfo ; tex for tangling PDFs
                                       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 isync lua5.3 luarocks ; email
                                       x11-utils wmctrl xdotool ; used by thyme
                                       graphviz graphviz-doc
                                       ;; fonts
                                       unifont ttf-ancient-fonts fonts-inconsolata xfonts-terminus
                                       fonts-vollkorn
                                       ttf-cascadia-code ttf-anonymous-pro fonts-hack-ttf)))))


(dolist (pkg packages-to-install)
  (system-packages-install pkg))


(require 'subr-x)

Design & Layout

I've taken the bulk of these from the elegant-emacs repository, which makes things look much simpler.

(set-face-attribute 'default nil
                    :font "ProfontIIx"
                    :weight 'light
                    :height 120)

;; Set the fixed pitch face
(set-face-attribute 'fixed-pitch nil
                    :font "ProfontIIx"
                    :weight 'light
                    :height 120)

;; Set the variable pitch face
(set-face-attribute 'variable-pitch nil
                    ;; :font "Cantarell"
                    :font "Iosevka Aile"
                    :height 180
                    :weight 'light)

(menu-bar-mode -1) ;; minimal chrome
(tool-bar-mode -1) ;; no toolbar
(scroll-bar-mode -1) ;; disable scroll bars

(set-frame-parameter (selected-frame)
                     'internal-border-width 24)
(set-frame-parameter (selected-frame) 'alpha '(85 . 50))
(setq default-frame-alist
      (append (list '(width  . 80) '(height . 40)
                    '(vertical-scroll-bars . nil)
                    '(internal-border-width . 24))))


;; Line spacing, can be 0 for code and 1 or 2 for text
(setq-default line-spacing 0)

;; Underline line at descent position, not baseline position
(setq x-underline-at-descent-line t)

;; No ugly button for checkboxes
(setq widget-image-enable nil)

;; Line cursor and no blink
(set-default 'cursor-type  '(bar . 3))
(blink-cursor-mode 0)

;; No sound
(setq visible-bell t)
(setq ring-bell-function 'ignore)

;; Paren mode is part of the theme
(show-paren-mode t)

;; No fringe but nice glyphs for truncated and wrapped lines
(fringe-mode '(0 . 0))
(defface fallback '((t :family "Fira Code Light")) "Fallback")
;; (set-display-table-slot standard-display-table 'truncation
;;                         (make-glyph-code ?… 'fallback))
;; (set-display-table-slot standard-display-table 'wrap
;;                         (make-glyph-code ?↩ 'fallback))

;; simplified mode line
(defun mode-line-render (left right)
  (let* ((available-width (- (window-width) (length left) )))
    (format (format "%%s %%%ds" available-width) left right)))
(setq-default mode-line-format
              '((:eval
                 (mode-line-render
                  (format-mode-line (list
                                     (propertize "☰" 'face `(:inherit mode-line-buffer-id)
                                                 'help-echo "Mode(s) menu"
                                                 'mouse-face 'mode-line-highlight
                                                 'local-map   mode-line-major-mode-keymap)
                                     " %b "))
                  (format-mode-line "%4l:%2c  ")))))

;; move modeline to the top of the buffer
;; Not sure if I like this yet..
(setq-default header-line-format mode-line-format)
(setq-default mode-line-format nil)
(setq display-time-format "%l:%M %p %b %y")
(setq display-time-default-load-average nil)


;; Vertical window divider
(setq window-divider-default-right-width 3)
(setq window-divider-default-places 'right-only)
(window-divider-mode)

Setup diminish, which reduces the clutter in the modeline.

(use-package diminish)

Programming Languages

General

Ensure our files don't have weird trailing whitespace.

(use-package ws-butler
  :hook ((text-mode . ws-butler-mode)
         (prog-mode . ws-butler-mode)))

Setup editorconfig for projects that use that.

(use-package editorconfig
  :config
  (editorconfig-mode 1)
  (setq editorconfig-trim-whitespaces-mode
        'ws-butler-mode))

Highlight things which are under the point.

(use-package idle-highlight-mode
  :hook prog-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
  :init (setq lsp-keymap-prefix "C-;")
  :hook (((js2-mode yaml-mode go-mode typescript-mode) . lsp)
         (lsp-mode . lsp-enable-which-key-integration))
  :config (progn
            (define-key lsp-mode-map (kbd "M-.") 'lsp-goto-implementation)
            (let ((eslint-language-server-file "/home/abrahms/src/github.com/microsoft/vscode-eslint/server/out/eslintServer.js"))
              (if (file-exists-p eslint-language-server-file)
                  (setq lsp-eslint-server-command '("node" eslint-language-server-file "--stdio"))
                (message "You're missing the vscode eslint plugin. Currently, you have to manually install it."))))
  :bind (:map lsp-mode-map
         ("TAB" . completion-at-point))
  :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")
   (gopls . "GO111MODULE=on go install golang.org/x/tools/gopls@latest")))

(use-package lsp-python-ms
  :init (setq lsp-python-ms-auto-install-server t)
  :hook (python-mode . (lambda ()
                         (require 'lsp-python-ms)
                         (lsp))))

(use-package lsp-ui
  :hook lsp-mode
  ;; flycheck integration & higher level UI modules
  :commands lsp-ui-mode
  :config
  (setq lsp-ui-sideline-enable t)
  (setq lsp-ui-sideline-show-hover nil)
  (setq lsp-ui-doc-position 'bottom)
  (lsp-ui-doc-show))

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

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.

(use-package paredit)
(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)
(add-hook '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)

(setq inferior-lisp-program "sbcl")

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.

(use-package slime)

With lots of parens, it's easy to get lost. Fix that.

(use-package rainbow-delimiters
  :hook (prog-mode . rainbow-delimiters-mode))

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!

(use-package virtualenvwrapper
  :init
  (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.

(use-package pony-mode
  :config
  (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

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

  (add-hook 'js2-mode-hook 'nvm-use-for-buffer)
  (add-hook 'js2-mode-hook 'add-node-modules-path)

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

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

;; improve colors in terminals
(use-package eterm-256color
  :hook (term-mode))

(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"))
  :init (setq vterm-always-compile-module t)
  :config '(
           (setq vterm-term-environment-variable "eterm-256color")
           (setq vterm-kill-buffer-on-exit t)))

Rust

Simple addition of the rust mode.

(use-package rust-mode
  :mode "\\.rs\\'"
  :bind ("C-c r" . rust-run)
  :init (setq rust-format-on-save t))
(use-package cargo
  :defer t)

GraphQL

(use-package graphql-mode)

Ruby

(use-package enh-ruby-mode) ; better ruby mode
(use-package inf-ruby) ; irb in separate pane
(use-package rspec-mode) ; ruby testing mode

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

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

Keep ctags up to date.

(use-package ctags-update)

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
  :config
  (if (file-exists-p (expand-file-name "~/.nvm"))
      (let ((nvm-version "v14.18.1"))
        (condition-case nil
            (nvm-use nvm-version)
          (error (message "You tried to activate version %s, but it isn't installed" nvm-version))))))

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.

(use-package typescript-mode
  :mode "\\.ts\\'"
  :init (setq typescript-indent-level 2))

CSS / HTML authoring

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)
(use-package '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.

(use-package web-mode
  :config

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

Setup various things for scss / less

(use-package scss-mode)
(use-package flymake-sass)
(use-package less-css-mode)

Easy html authoring tools

(use-package zencoding-mode)

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 clojure-mode)
(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"))

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 install -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 install -u golang.org/x/tools/cmd/goimports")
   (godef . "go install -u github.com/rogpeppe/godef")
   (gopls . "go install -u golang.org/x/tools/gopls@latest")
   (gocode . "go install -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)

Misc coding modes

(use-package protobuf-mode)
(use-package json-mode) ;; better syntax highlighting / error reporting for json
(use-package nginx-mode)
(use-package yaml-mode)
(use-package haskell-mode) ;; for xmonad configs
(use-package dockerfile-mode) ;; Dockerfiles
(use-package terraform-mode)
(use-package lua-mode)

Reading

I've been experimenting with adding RSS feeds to emacs. We'll see how this goes!

(use-package elfeed
  ;; :bind ("C-x f" . elfeed)

  :config (setq elfeed-feeds'(
                              ("https://updates.orgmode.org/feed/updates" emacs)
                              "http://brooker.co.za/blog/rss.xml"
                              "http://getpocket.com/users/abrahms/feed/unread"
                              "https://api.quantamagazine.org/feed/"
                              ("http://sigusr2.net/rss.xml" friend)
                              ("http://feeds.feedburner.com/gregnewman-combined" friend)
                              ("https://push.cx/feed" friend)
                              ("http://www.bobhancock.org/feeds/posts/default" friend)
                              ("https://thejoyofmissingoutblog.wordpress.com/feed/" friend)
                              ("https://justinjackson.ca/feed" friend)
                              "http://lethain.com/feeds/all/"
                              "http://bestofwikipedia.tumblr.com/rss"
                              "https://emilygorcenski.com/index.xml"
                              "http://bullshit.tumblr.com/rss"
                              ("http://feeds.feedburner.com/NoTechMagazine" solarpunk)
                              ("https://solarpunkstation.wordpress.com/feed/" solarpunk)
                              ("http://solarpunkdruid.com/feed/" solarpunk)
                              "http://codeascraft.com/feed/"
                              "https://boyter.org/index.xml"
                              "http://rss.acm.org/dl/J780.xml"
                              "https://www.elidedbranches.com/feeds/posts/default"
                              "http://rss.acm.org/dl/J401.xml"
                              "http://thecodelesscode.com/rss"
                              "https://freedom-to-tinker.com/feed/"
                              "http://rss.acm.org/dl/J790.xml"
                              ("http://feeds.feedburner.com/typepad/krisdedecker/lowtechmagazineenglish" solarpunk)
                              "https://danluu.com/atom.xml"
                              "https://www.spakhm.com/feed/"
                              "https://sive.rs/en.atom"
                              "http://feeds.feedburner.com/theendeavour"
                              "https://jvns.ca/atom.xml"

                              "http://feeds.feedburner.com/montylounge-posts/"
                              "http://charlesleifer.com/blog/rss/"
                              "https://alexgaynor.net/feed.xml"
                              "http://feeds.feedburner.com/DougHellmann"
                              "https://blog.elliotmurphy.com/rss/"
                              "http://feeds2.feedburner.com/EricsThoughts"
                              "http://blog.michaeltrier.com/feed/atom.xml"
                              "https://lucumr.pocoo.org/feed.atom"
                              "http://feeds.feedburner.com/b-list-entries"
                              "http://feeds.feedburner.com/patrickaltman/"
                              "http://www.saltycrane.com/feeds/latest/"
                              "http://feeds.feedburner.com/justinlilly/"
                              "https://lincolnloop.com/blog/feed/"
                              "https://eldarion.com/rss.xml"
                              "https://cpbotha.net/posts/index.xml"
                              "http://www.dabeaz.com/blog/atom.xml"
                              "https://blog.acolyer.org/feed/"
                              "https://www.gatesnotes.com/home/rss"
                              "http://feeds.feedburner.com/vivekhaldar"
                              "http://www.43folders.com/rss.xml"
                              "http://www.willmcgugan.com/blog/tech/feeds/posts/"
                              "http://feeds.feedburner.com/andrzejkrzywda?format=xml"
                              "http://blog.platypope.org/syndicate"
                              "https://emacs.wordpress.com/feed/"
                              "http://emacs-fu.blogspot.com/feeds/posts/default"
                              "https://www.artima.com/cppsource/feeds/cppsource.rss"
                              "http://feeds.feedburner.com/emacslife"
                              "http://feeds.feedburner.com/emacsblog"
                              "http://reddit.project.samueltaylor.org/sub/emacs"

                              ;; From the tech writers group
                              "https://blog.pragmaticengineer.com/feed/"
                              "https://how-possible.blog/feed/"
                              "https://dylanamartin.com/blog.xml"
                              "http://www.kylestratis.com/post/rss.xml"
                              "https://robertovitillo.com/rss.xml"
                              "https://www.dmellonielet.com/feed.xml"
                              "https://www.theengineeringmanager.com/feed/"
                              "https://blog.odoom.net/rss/"
                              "https://frontier.substack.com/feed/"
                              "https://cogitocoders.com/feed/"
                              "https://evanm.website/atom.xml"
                              "https://ochronus.online/feed.xml"
                              "https://blog.thepete.net/blog/index.xml"
                              "https://sujithjay.com/atom.xml"
                              "https://boxkitemachine.net/index.xml"
                              "https://www.benkuhn.net/index.xml"
                              "https://www.averywlittle.dev/essays/rss/"
                              "https://nemethgergely.com/feed.xml"
                              "https://cogitocoders.com/feed/"
                              "https://xdg.me/writing/index.xml"
                              "https://tndl.me/rss.xml"
                              "https://gsong.dev/articles/rss.xml"
                              "https://managingdev.com/feed/"
                              "https://dballona.com/en/feed.xml"
                              "https://blog.davidtate.org/rss/"
                              "https://commoncog.com/blog/rss/"
                              "https://rumproarious.com/index.xml"
                              "http://sinisterlight.com/index.xml"
                              "https://thorstenball.com/atom.xml"
                              "https://www.jonathanyeong.com/feed/feed.xml"
                              "https://tusharc.dev/index.xml"
                              "https://www.calebontiveros.com/rss/"
                              "https://gummadi.me/feed/"
                              "https://www.samueltaylor.org/articles/feed.xml"
                              "https://aless.co/rss.xml"
                              "https://www.susanshu.com/feed.xml"

                              "https://blog.pjam.me/index.xml"
                              "https://vegardstikbakke.com/feed.xml"
                              "https://v5.chriskrycho.com/feed.xml"
                              "https://martinapugliese.github.io/feed.xml"
                              )
                )
  )

To that end, I've found when reading along with the in the gemini spec, that being able to pull up RFCs is of use.

(use-package rfc-mode
  :config (setq rfc-mode-directory (expand-file-name "~/src/rfc-editor.org/")))

Writing

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)
  :init
  (add-hook 'markdown-mode-hook #'visual-line-mode)
  (add-hook 'markdown-mode-hook #'variable-pitch-mode)
  (add-hook 'markdown-mode-hook #'flyspell-mode)
  :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)

Text

There are common technical words that aren't a part of the dictionary. Provide a handy shortcut for adding them.

(require 'flyspell)
(defun abrahms/flyspell-save-word ()
  (interactive)
  (let ((current-location (point))
         (word (flyspell-get-word)))
    (when (consp word)
      (flyspell-do-correct 'save nil (car word) current-location (cadr word) (caddr word) current-location))))

When editing raw text, it's nice to have a simple spellcheck. For this, we use flyspell.

(define-key flyspell-mode-map (kbd"\C-c$") 'abrahms/flyspell-save-word)

Org-Mode

Preamble

  • Screenshots

    It's really helpful to be able to include screenshots in my org-mode setup. This seems to work for windows, though there are nice packages (org-screenshot, org-download) for the non-windows case.

    (require 'org-screenshot) ;; comes from org-contrib
    (customize-set-variable 'org-screenshot-image-directory "~/docs/images/")
    
    (defun ja/joindirs (root &rest dirs)
      "Joins a series of directories together, like Python's os.path.join,
      (dotemacs-joindirs \"/tmp\" \"a\" \"b\" \"c\") => /tmp/a/b/c"
      ; via https://newbedev.com/what-is-the-correct-way-to-join-multiple-path-components-into-a-single-complete-path-in-emacs-lisp
    
      (if (not dirs)
          root
        (apply 'joindirs
               (expand-file-name (car dirs) root)
               (cdr dirs))))
    
    (defun ja/windows-screenshot ()
      "Take a screenshot into a time stamped unique-named file in a sub-directory of the org-buffer and insert a link to this file."
      ; via https://www.sastibe.de/2018/11/take-screenshots-straight-into-org-files-in-emacs-on-win10/
      (interactive)
      (setq filename
            (concat
             (make-temp-name
              (ja/joindirs (file-name-directory buffer-file-name)
                           "screenshots"
                           (concat (file-name-nondirectory buffer-file-name)
                                   "_"
                                   (format-time-string "%Y%m%d_%H%M%S_")) )) ".png"))
      ;; (message "filename: " filename)
      (let ((dirname (file-name-directory filename)))
        (message (concat "dirname: " dirname))
        (unless (file-exists-p dirname)
          (make-directory dirname)))
      (shell-command "snippingtool /clip")
      (shell-command (concat "powershell -command \"Add-Type -AssemblyName System.Windows.Forms;if ($([System.Windows.Forms.Clipboard]::ContainsImage())) {$image = [System.Windows.Forms.Clipboard]::GetImage();[System.Drawing.Bitmap]$image.Save('" filename "',[System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'clipboard content saved as file'} else {Write-Output 'clipboard does not contain image data'}\""))
      (insert (concat "[[file:" filename "]]"))
      (org-display-inline-images))
    

    org-screenshot has a requirement on scrot which doesn't exist as a binary for OSX. So install a simple wrapper around the screencapture tool it has.

    screencapture "$@"
    

org-mode itself

(defun abrahms/org-mode-setup ()
  (org-indent-mode)
  (variable-pitch-mode 1)
  (auto-fill-mode 0)
  (visual-line-mode 1)
  (diminish org-indent-mode))

(use-package org
  :defer t
  :bind (("\C-ca" . org-agenda)
         :map org-mode-map
         ("C-c s" . ja/windows-screenshot))
  :hook ((org-mode . abrahms/org-mode-setup))

  :config
  (setq org-ellipsis " ▾"
        org-hide-emphasis-markers t
        org-src-fontify-natively t
        org-fontify-quote-and-verse-blocks t
        org-src-tab-acts-natively t
        org-edit-src-content-indentation 2
        org-hide-block-startup nil

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.

org-confirm-babel-evaluate nil

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

       org-todo-keywords
              '((sequence "TODO" "NEXT" "WAITING" "BLOCKED" "DONE"))
        org-src-preserve-indentation t
        org-startup-folded 'content
        org-cycle-separator-lines 2
        org-modules nil
        org-directory (cond
                             ;; it's in the roaming data
                             ((eq system-type 'windows-nt) "\\..\\..\\docs\\")
                             ;; done via syncthing
                             ((eq system-type 'darwin) (concat (getenv "HOME") "/docs/"))
                             ;; use dropbox on home computer
                             (t (concat (getenv "HOME") "/Dropbox/docs/")))
         org-agenda-files (directory-files-recursively org-directory "org$")
         org-refile-targets '((nil :maxlevel . 9)
                                        (org-agenda-files :maxlevel . 9))
         org-outline-path-complete-in-steps nil         ; Refile in a single go
         org-refile-use-outline-path t                  ; Show full paths for refiling
         org-log-done 'note

        )

  (set-face-attribute 'org-document-title nil :font "Iosevka Aile" :weight 'bold :height 1.3)
  (dolist (face '((org-level-1 . 1.2)
                  (org-level-2 . 1.1)
                  (org-level-3 . 1.05)
                  (org-level-4 . 1.0)
                  (org-level-5 . 1.1)
                  (org-level-6 . 1.1)
                  (org-level-7 . 1.1)
                  (org-level-8 . 1.1)))
    (set-face-attribute (car face) nil :font "Iosevka Aile" :weight 'medium :height (cdr face)))



;; Make sure org-indent face is available
(require 'org-indent)

;; Ensure that anything that should be fixed-pitch in Org files appears that way
(set-face-attribute 'org-block nil :foreground nil :inherit 'fixed-pitch)
(set-face-attribute 'org-table nil  :inherit 'fixed-pitch)
(set-face-attribute 'org-formula nil  :inherit 'fixed-pitch)
(set-face-attribute 'org-code nil   :inherit '(shadow fixed-pitch))
(set-face-attribute 'org-indent nil :inherit '(org-hide fixed-pitch))
(set-face-attribute 'org-verbatim nil :inherit '(shadow fixed-pitch))
(set-face-attribute 'org-special-keyword nil :inherit '(font-lock-comment-face fixed-pitch))
(set-face-attribute 'org-meta-line nil :inherit '(font-lock-comment-face fixed-pitch))
(set-face-attribute 'org-checkbox nil :inherit 'fixed-pitch)

;; Get rid of the background on column views
(set-face-attribute 'org-column nil :background nil)
(set-face-attribute 'org-column-title nil :background nil)
;; maybe https://github.com/alphapapa/magit-todos ?
) ; close opening from org block.

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.

(use-package diary-lib
  :config
  (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)

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
              '(
                (:todo "NEXT")
                (:priority "A" :name "High priority")
                (:deadline t :name "Upcoming deadlines")
                (:scheduled today :name "today")
                (:scheduled past :name "past")
                (:file-path "notes.org" :order 11 :name "Home stuff")
                (:tag "writing" :order 10)
                (:discard (:scheduled future))
                (:auto-property "WAITING_ON" :log t :name "Waiting on people")
                (:auto-property "PROJECT" :log t)
                (:auto-tags)
                (:auto-todo)))

  :config (org-super-agenda-mode 1)
  (variable-pitch-mode 1))

I've started to use org-datetree as my header entry. I don't want to log days, just weeks and their action items. This changes the datetree system to do that.

(eval-after-load "org-datetree"
  ;; This is a custom org-date tree which doesn't insert dates, just
  ;; week numbers. via https://emacs.stackexchange.com/a/60851/8777
  '(defun org-datetree-find-iso-week-create (d &optional keep-restriction)
     "Find or create an ISO week entry for date D.
Compared to `org-datetree-find-date-create' this function creates
entries ordered by week instead of months.
When it is nil, the buffer will be widened to make sure an existing date
tree can be found.  If it is the symbol `subtree-at-point', then the tree
will be built under the headline at point."
     (setq-local org-datetree-base-level 1)
     (save-restriction
       (if (eq keep-restriction 'subtree-at-point)
           (progn
             (unless (org-at-heading-p) (error "Not at heading"))
             (widen)
             (org-narrow-to-subtree)
             (setq-local org-datetree-base-level
                         (org-get-valid-level (org-current-level) 1)))
         (unless keep-restriction (widen))
         ;; Support the old way of tree placement, using a property
         (let ((prop (org-find-property "WEEK_TREE")))
           (when prop
             (goto-char prop)
             (setq-local org-datetree-base-level
                         (org-get-valid-level (org-current-level) 1))
             (org-narrow-to-subtree))))
       (goto-char (point-min))
       (require 'cal-iso)
       (let* ((year (calendar-extract-year d))
              (month (calendar-extract-month d))
              (day (calendar-extract-day d))
              (time (encode-time 0 0 0 day month year))
              (iso-date (calendar-iso-from-absolute
                         (calendar-absolute-from-gregorian d)))
              (weekyear (nth 2 iso-date))
              (week (nth 0 iso-date)))
         ;; ISO 8601 week format is %G-W%V(-%u)
         (org-datetree--find-create
          "^\\*+[ \t]+\\([12][0-9]\\{3\\}\\)\\(\\s-*?\
\\([ \t]:[[:alnum:]:_@#%%]+:\\)?\\s-*$\\)"
          weekyear nil nil
          (format-time-string "%G" time))
         (org-datetree--find-create
          "^\\*+[ \t]+%d-W\\([0-5][0-9]\\)$"
          weekyear week nil
          (format-time-string "%G-W%V" time))))))

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

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

Support exporting to markdown.

(require 'ox-md)

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.

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

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+datetree ,(concat org-directory "eng-log.org") "Engineering Worklog") "**** TODO %?\n%a" :tree-type week)
        ("m" "Meeting" entry (file+olp+datetree ,(concat org-directory "eng-log.org") "Engineering Worklog") "**** %?\n%t" :tree-type week)
        ("i" "Item" entry (file+olp+datetree ,(concat org-directory "eng-log.org") "Engineering Worklog") "**** %?\n%a" :tree-type week)
        ("a" "Action item" entry (file+olp+datetree ,(concat org-directory "eng-log.org") "Engineering Worklog") "* WAITING %?\n:PROPERTIES:\n:WAITING_ON: %^{Who owns this?}\n:END:\n %i %U %a" :tree-type week)
        ("p" "Perf Note" entry (file+olp+datetree ,(concat org-directory "eng-log.org") "Engineering Worklog") "* %? :perf:\n\n %i %U" :tree-type week)
        ("c" "org-capture selected" entry (file+olp+datetree ,(concat org-directory "eng-log.org") "Engineering Worklog")
         "* [[%:link][%:description]]\n #+BEGIN_QUOTE\n%i\n#+END_QUOTE\n\n\n%?" :tree-type week)
        ("C" "org-capture unselected" entry (file+olp+datetree ,(concat org-directory "eng-log.org") "Engineering Worklog")
         "* %? [[%:link][%:description]] \nCaptured On: %U" :tree-type week)
        ))

I wrote my first emacs plugin to be listed on melpa (to my memory, at least) which hooks into org mode in order to output gemini sites.

(use-package ox-gemini)

Mermaid is a tool to generate nice sequence diagrams.

(use-package ob-mermaid
  :after org
  :config (setq ob-mermaid-cli-path "/usr/local/bin/mmdc")
  :ensure-system-package "mermaid-cli")

When making TODO entries, give them a created time. I think this will be helpful to track down stale tasks & generally get metrics around throughput or similar when I bother to collect that data.

(use-package org-contrib)

(require 'org-expiry)

;; via https://stackoverflow.com/q/12262220/4972
(setq org-treat-insert-todo-heading-as-state-change t)
(add-hook 'org-after-todo-state-change-hook
          (lambda ()
            (when (string= org-state "TODO")
              (save-excursion
                (org-back-to-heading)
                (org-expiry-insert-created)))))

When refiling a headline, especially from my notes.. it's nice to leave a breadcrumb back to the place it was moved to so I can preserve the org-roam mappings. via

(defvar my/org-last-refile-marker nil)
(defvar my/org-last-refile-link nil)

(advice-add 'org-refile
            :before
            (lambda (&rest _)
              (save-excursion
                (org-back-to-heading)
                (setq my/org-last-refile-marker (point-marker))))
            '((name . "my/org-set-refile-marker")))

(defun my/org-set-last-refile-link ()
  (setq my/org-last-refile-link (org-store-link nil)))

(add-hook 'org-after-refile-insert-hook #'my/org-set-last-refile-link)

(advice-add 'org-refile
            :after
            (lambda (&rest _)
              (when (and my/org-last-refile-marker
                         my/org-last-refile-link)
                (let ((buf (marker-buffer my/org-last-refile-marker)))
                  (when (buffer-live-p buf)
                    (with-current-buffer buf
                      (save-excursion
                        (goto-char my/org-last-refile-marker)
                        (insert (concat my/org-last-refile-link "\n"))))))
                (setq my/org-last-refile-marker nil)
                (setq my/org-last-refile-link nil)))
            '((name . "my/org-insert-refile-marker")))

After Org

These things load after org is done.

(use-package org-superstar
  :after org
  :hook (org-mode . org-superstar-mode)
  :custom
  (org-superstar-remove-leading-stars t)
  (org-superstar-headline-bullets-list '("◉" "○" "●" "○" "●" "○" "●")))

(use-package org-appear
  :after org
  :hook (org-mode . org-appear-mode))

Setup the look and feel of org-mode such that we have a non-monospaced font in the text portions. Olivetti is a nice writing mode. Variable pitch is what does the work of making it mixed font.

(use-package olivetti
  :disabled
  :hook (org-mode . olivetti-mode))


(use-package org-variable-pitch
  :init (org-variable-pitch-setup)
  :hook (org-mode . org-variable-pitch-minor-mode))

Org protocol

Org protocol is a mechanism where we can kick off emacs workflows (e.g. capturing todos) from external apps.

(require 'org-protocol)

We also need a desktop entry to tell Ubuntu how to load org-protocol links. When this is done, we must manually run update-desktop-database ~/.local/share/applications/

[Desktop Entry]
Name=org-protocol
Exec=/usr/bin/emacsclient -n %u
Type=Application
Terminal=false
Categories=System;
MimeType=x-scheme-handler/org-protocol;

You'll also need to change the preferences for the org-capture plugin to use the correct capture template. c is for selected, C is for unselected. If you want to prevent the "are you sure?" popups when you attempt to capture, turn security.external_protocol_requires_permission off.

Org roam

I've moved the bulk of my note taking to org-roam. In large part because building a personal wiki seems really valuable to me as it accumulates knowledge over the years.

(use-package org-roam
  :after org
  :ensure t
  :bind (("C-c n l" . org-roam-buffer-toggle)
         ("C-c n f" . org-roam-node-find)
         ("C-c n g" . org-roam-graph)
         ("C-c n i" . org-roam-node-insert)
         ("C-c n c" . org-roam-capture)
         ;; Dailies
         ("C-c n j" . org-roam-dailies-capture-today))
  :init
  (setq org-roam-v2-ack t
        org-roam-directory (concat org-directory "roam/"))

  :config
  (org-roam-db-autosync-mode)

  ;; link to my journal node on dailies insertion
  (setq org-roam-dailies-capture-templates
      '(("d" "Journal" entry "* %?"
         :if-new (file+head+olp "%<%Y-%m-%d>.org"
                                "#+title: %<%Y-%m-%d>\n[[id:81A8F7B2-5E27-4469-9473-97AAF9BFA104][journal]]\n"
                                ("Journal")))))

  ;; If using org-roam-protocol
  (require 'org-roam-protocol))

Deft is a tool which will help looking through a large number of files. I like it so far. My biggest complaint is that when you invoke it again after finding your item, it will still be populated with your last search phrase.

;; used to index notes. Can look at https://github.com/hasu/notdeft if that ever
;; gets slow.
(use-package deft
  :bind
  ("C-c n d" . deft)
  :config
  (setq
   deft-recursive t
   deft-use-filename-as-title t
   deft-use-filter-string-for-filename t
   deft-default-extension "org"
   deft-directory org-roam-directory))

;; capture screenshots
(use-package org-download
  :after org
  :config
  (setq org-download-screenshot-method "screencapture -i %s")
  :bind
  (:map org-mode-map
        (("s-Y" . org-download-screenshot)
         ("s-y" . org-download-yank))))

;; look into org-fc for spaced repitition


;; From org-roam-ui: https://github.com/org-roam/org-roam-ui#manually b/c they don't have a melpa release
(use-package websocket)
(use-package simple-httpd)

(use-package org-roam-ui
  :config
  (setq org-roam-ui-sync-theme t
        org-roam-ui-follow t
        org-roam-ui-update-on-save t
        org-roam-ui-open-on-start t))

(use-package org-roam-timestamps)

This vulpea stuff is an optimization that will allow limiting org-agenda to the roam files which have TODO entries. This will become more important over time as I accumulate a large number of entries.

;; vulpea stuff from https://d12frosted.io/posts/2021-01-16-task-management-with-roam-vol5.html
(use-package vulpea
  :after org-roam
  ;; hook into org-roam-db-autosync-mode you wish to enable
  ;; persistence of meta values (see respective section in README to
  ;; find out what meta means)
  :hook ((org-roam-db-autosync-mode . vulpea-db-autosync-enable)))

(defun vulpea-project-p ()
  "Return non-nil if current buffer has any todo entry.

TODO entries marked as done are ignored, meaning the this
function returns nil if current buffer contains only completed
tasks."
  (seq-find                             ; (3)
   (lambda (type)
     (eq type 'todo))
   (org-element-map                         ; (2)
       (org-element-parse-buffer 'headline) ; (1)
       'headline
     (lambda (h)
       (org-element-property :todo-type h)))))

(defun vulpea-project-update-tag ()
    "Update PROJECT tag in the current buffer."
    (when (and (not (active-minibuffer-window))
               (vulpea-buffer-p))
      (save-excursion
        (goto-char (point-min))
        (let* ((tags (vulpea-buffer-tags-get))
               (original-tags tags))
          (if (vulpea-project-p)
              (setq tags (cons "project" tags))
            (setq tags (remove "project" tags)))

          ;; cleanup duplicates
          (setq tags (seq-uniq tags))

          ;; update tags if changed
          (when (or (seq-difference tags original-tags)
                    (seq-difference original-tags tags))
            (apply #'vulpea-buffer-tags-set tags))))))

(defun vulpea-buffer-p ()
  "Return non-nil if the currently visited buffer is a note."
  (and buffer-file-name
       (string-prefix-p
        (expand-file-name (file-name-as-directory org-roam-directory))
        (file-name-directory buffer-file-name))))

(defun vulpea-project-files ()
    "Return a list of note files containing 'project' tag." ;
    (seq-uniq
     (seq-map
      #'car
      (org-roam-db-query
       [:select [nodes:file]
        :from tags
        :left-join nodes
        :on (= tags:node-id nodes:id)
        :where (like tag (quote "%\"project\"%"))]))))

(defun vulpea-agenda-files-update (&rest _)
  "Update the value of `org-agenda-files'."
  ;;; Not yet fully on org-roam, so also include things in the top level directory
  (setq org-agenda-files (append
                          (remove-if
                           (lambda (x)
                             (string-match (regexp-quote ".#") x))
                           (directory-files org-directory t "org$"))
                          (vulpea-project-files))))

(require 'vulpea)
(add-hook 'find-file-hook #'vulpea-project-update-tag)
(add-hook 'before-save-hook #'vulpea-project-update-tag)

(advice-add 'org-agenda :before #'vulpea-agenda-files-update)

When trying to find which of my projects were stuck, I look through all projects to determine if they have todo entries or not. I'd like to figure out a UI to display these, but not there quite yet.

(defun abrahms/is-org-file-stuck (f)
  "given a filename, determine if it's stuck

Stuck means it has a 'project' property, but doesn't have TODOs"
  (let ((go-back-to (current-buffer)))
    (find-file f)
    (let* ((is-project (not (eq nil (org-entry-get (point-min) "project" t))))
           (is-stuck (and is-project (not (vulpea-project-p)))))
      (switch-to-buffer go-back-to)
      is-stuck)))

(defun abrahms/stuck-projects ()
  "Gathers a list of org-files with a PROJECT property but without any associated TODOs"
  (interactive)
  (let ((buffer-name "*stuck-projects*"))
    (with-help-window buffer-name
      (princ "Stuck ones:\n")
      (princ
       (mapconcat 'identity
                  (seq-filter 'abrahms/is-org-file-stuck
                              (mapcar #'cadr (org-roam-db-query [:select *
                                   :from nodes
                                   :where (like properties (quote "%PROJECT%"))
                                   ])))
                  "\n")))
    (switch-to-buffer-other-window buffer-name)))

There's not native support for refiling a subtree to a new node while going through the "name this appropriately" flow.

(defun abrahms/org-roam-refile-new-node ()
  ;; via https://org-roam.discourse.group/t/creating-an-org-roam-note-from-an-existing-headline/978
  "Create an Org-roam note from the current headline and jump to it.

Normally, insert the headline’s title using the ’#title:’ file-level property
and delete the Org-mode headline. However, if the current headline has a
Org-mode properties drawer already, keep the headline and don’t insert
‘#+title:'. Org-roam can extract the title from both kinds of notes, but using
‘#+title:’ is a bit cleaner for a short note, which Org-roam encourages."
  (interactive)
  (let ((title (nth 4 (org-heading-components)))
        (has-properties (org-get-property-block)))
    (org-cut-subtree)
    (org-roam-node-find 'other-window title nil)
    (org-paste-subtree)
    (unless has-properties
      (kill-line)
      (while (outline-next-heading)
        (org-promote)))
    (goto-char (point-min))
    (when has-properties
      (kill-line)
      (kill-line))))

Blogging

I tried porting my blog over to org-static-blog, but found the lack of static file handling to be troublesome. Instead, I just use standard org-mode html export.

(require 'org)
(require 'ox-publish)
(use-package gnuplot)
(require 'ob-gnuplot)
(require 'ox-html)
(require 'htmlize)
(require 'ox-latex)

(defun cleanup-html-output ()
  (beginning-of-buffer)
  (replace-string "&#57345;" "")  
  (beginning-of-buffer)
  (replace-string "&#57344;" ""))

(setq org-html-head-extra " 
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta charset='utf-8'>
<meta name='description' content='The blog of Justin Abrahms, which discusses operations & programming with an eye towards beginners.'>
<link rel='alternate' type='application/rss+xml' href='/rss.xml' title='Justin Abrahms'>
<link href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css' rel='stylesheet'>
<link href='/static/css/simpleness.css' rel='stylesheet' type='text/css'>

<link href=\"//cdn-images.mailchimp.com/embedcode/slim-081711.css\" rel=\"stylesheet\" type=\"text/css\">
<style type=\"text/css\"> 
#mc_embed_signup{clear:left; font:14px Helvetica,Arial,sans-serif; width: 400px; margin: 0 auto; }

")



(add-hook 'htmlize-after-hook 'cleanup-html-output)
(setq org-export-latex-default-class "IEEEtran")

(defun org-publish-attachment-and-make-readable (plist filename pub-dir)
  (org-publish-attachment plist filename pub-dir)
  (chmod
   (expand-file-name (file-name-nondirectory filename) pub-dir)
   (file-modes-symbolic-to-number "oug+rw")))

(defun stats-tracking-code (_) "
                  <!-- stats tracking -->
                  <script type=\"text/javascript\">
                    var _gaq = _gaq || [];
                    _gaq.push(['_setAccount', 'UA-615313-5']);
                    _gaq.push(['_setDomainName', 'abrah.ms']);
                    _gaq.push(['_trackPageview']);
                    (function() {
                      var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
                      ga.src = ('https:' == document.location.protocol ? 'https://' : 'http://') + 'stats.g.doubleclick.net/dc.js';
                      var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
                    })();
                  </script>
                  <!-- end stats tracking -->")

(defun my-org-confirm-babel-evaluate (lang body)
  (not (string= lang "gnuplot")))  ; don't ask for gnuplot

(setq
 org-confirm-babel-evaluate 'my-org-confirm-babel-evaluate
 org-html-htmlize-output-type 'css
 org-link-file-path-type 'relative
 org-publish-project-alist
 '(("blog"
    :components ("blog-content" "blog-static" "blog-webmaster-tools" "blog-newsletter"))
   ("blog-newsletter"
    ;; newsletter confirmation pages for stats tracking purposes.
    :base-directory "~/src/justin.abrah.ms/blog/newsletter/"
    :publishing-directory "/ssh:justin@justin.abrah.ms:/srv/justin.abrah.ms/newsletter/"
    :base-extension "org"
    :publishing-function org-html-publish-to-html
    :author nil
    :with-toc nil
    :creator-info nil
    :html-postamble nil
    :html-preamble stats-tracking-code
    :timestamp nil
    :creator nil)
   ("blog-webmaster-tools"
    ;; google webmaster tools verification
    :base-directory "~/src/justin.abrah.ms/blog/"
    :publishing-directory "/ssh:justin@justin.abrah.ms:/srv/justin.abrah.ms/"
    :base-extension "html"
    :publishing-function org-publish-attachment-and-make-readable
    :author nil
    :with-toc nil
    :creator-info nil
    :html-postamble nil
    :html-preamble nil
    :timestamp nil
    :creator nil)
   ("blog-pastes"
    ;; placeholder directory to put pastes in for scpaste.
    :base-directory "~/src/justin.abrah.ms/blog/pastes/"
    :publishing-directory "/ssh:justin@justin.abrah.ms:/srv/justin.abrah.ms/pastes/"
    :base-extension "html"
    :publishing-function org-publish-attachment-and-make-readable
    :author nil
    :with-toc nil
    :creator-info nil
    :html-postamble nil
    :html-preamble nil
    :timestamp nil
    :creator nil)
   ("blog-content"
    :base-directory "~/src/justin.abrah.ms/blog/"
    :base-extension "org"
    :publishing-directory "/ssh:justin@justin.abrah.ms:/srv/justin.abrah.ms/"
    :recursive t
    :publishing-function org-html-publish-to-html
    :export-with-tags nil
    :headline-levels 4             ; Just the default for this project.
    :with-toc nil
    :section-numbers nil
    :sub-superscript nil
    :todo-keywords nil
    :author nil
    :creator-info nil
    :html-preamble (lambda (_) (concat (stats-tracking-code "") "
<nav><a href=\"/\">Justin Abrahms</a></nav>
<nav class=\"fontawesome\">
    <a href=\"https://twitter.com/justinabrahms\" target=\"_blank\">
        <i title=\"Twitter\" class=\"fab fa-twitter\"></i>
    </a>
    <a href=\"mailto:justin@abrah.ms\" target=\"_blank\">
        <i title=\"Email\" class=\"fas fa-at\"></i>
    </a>
    <a href=\"/rss.xml\" target=\"_blank\">
        <i title=\"RSS\" class=\"fas fa-rss\"></i>
    </a>
  </nav>"))
    :html-postamble "
<div class='foot'>
    &copy; 2012 - 2020 &#183; 
    <a href='/' >Home</a> &mdash; <a href='https://github.com/RainerChiang/simpleness'>Theme</a>
    <a href='#'><i class='fas fa-chevron-up'></i></a>
</div>

<hr /><!-- Begin MailChimp Signup Form -->  <div id=\"mc_embed_signup\"> <form action=\"https://gitstreams.us6.list-manage1.com/subscribe/post?u=b4fc0cc927878a024e46e1413&amp;id=7c57fe5fe7\" method=\"post\" id=\"mc-embedded-subscribe-form\" name=\"mc-embedded-subscribe-form\" class=\"validate\" target=\"_blank\" novalidate> <label for=\"mce-EMAIL\">Get notified when new articles are published</label> <input type=\"email\" value=\"\" name=\"EMAIL\" class=\"email\" id=\"mce-EMAIL\" placeholder=\"email address\" required> <div class=\"clear\"><input type=\"submit\" value=\"Subscribe\" name=\"subscribe\" id=\"mc-embedded-subscribe\" class=\"button\"></div> </form> </div> <!--End mc_embed_signup-->"
    :timestamp t
    :exclude-tags ("noexport" "todo")
    :auto-preamble f)
   ("blog-static"
    :base-directory "~/src/justin.abrah.ms/blog/static/"
    :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf\\|otf"
    :publishing-directory "/ssh:justin@justin.abrah.ms:/srv/justin.abrah.ms/static/"
    :recursive t
    :publishing-function org-publish-attachment-and-make-readable)
   ("literate-config"
    :publishing-directory "/ssh:justin@justin.abrah.ms:/srv/justin.abrah.ms/"
    :base-directory "~/src/justin.abrah.ms/blog/"
    :exclude ".*"
    :include ("dotfiles/emacs.org")
    :publishing-directory "/tmp/"
    :publishing-function org-html-publish-to-html
    )))


(use-package htmlize) ;; used for blog publishing

Scratchpad

Sometimes it's nice to have a scratch buffer. This lets you do that, but also have the relevant mode turned on.

(use-package scratch)

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)

Emacs miscellany

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.

(global-unset-key (kbd "s-t")) ;; disable osx font panel

(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-default fill-column 80) ;; M-q should fill at 80 chars, not 75
(setq initial-buffer-choice (concat org-directory "eng-log.org")) ;; make the eng log the first file that's open.

;; general programming things
;; 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 t) ;; auto-save buffer state on close for a later time.
(setq desktop-restore-eager 5) ;; don't load & render every file all at once. lazy load most of them in the background
(setq abbrev-file-name "~/.emacs.d/abbrev_defs") ;; where to save auto-replace maps

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

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)

(use-package xclip) ; used so you can paste into linux consoles

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

;; Windows has a special, odd setup.
(if (eq window-system 'w32)
    (setq browse-url-browser-function 'browse-url-default-windows-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))

Make the emacs built-in helpfiles more helpful.

(use-package helpful
  :custom
  (counsel-describe-function-function #'helpful-callable)
  (counsel-describe-variable-function #'helpful-variable)
  :bind
  ([remap describe-function] . helpful-function)
  ([remap describe-symbol] . helpful-symbol)
  ([remap describe-variable] . helpful-variable)
  ([remap describe-command] . helpful-command)
  ([remap describe-key] . helpful-key))

;; ensure the helpful package can find the associated c source code.
(setq source-directory (expand-file-name "~/src/emacs/"))

Ensure buffers are saved when we're not paying attention. Git will ensure we don't lose things.

(use-package super-save
  :defer 1
  :diminish super-save-mode
  :config
  (super-save-mode +1)
  (setq super-save-auto-save-when-idle t))

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

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")
;; loopback makes it ask me for my passphrase
(setq epa-pinentry-mode 'loopback)


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

It's also nice when you can read the size column. Let's make it human readable.

;; Use human readable Size column instead of original one
(define-ibuffer-column size-h
  (:name "Size" :inline t)
  (cond
   ((> (buffer-size) 1000000) (format "%7.1fM" (/ (buffer-size) 1000000.0)))
   ((> (buffer-size) 100000) (format "%7.0fk" (/ (buffer-size) 1000.0)))
   ((> (buffer-size) 1000) (format "%7.1fk" (/ (buffer-size) 1000.0)))
   (t (format "%8d" (buffer-size)))))

;; Modify the default ibuffer-formats
  (setq ibuffer-formats
        '((mark modified read-only " "
                (name 18 18 :left :elide)
                " "
                (size-h 9 -1 :right)
                " "
                (mode 16 16 :left :elide)
                " "
                filename-and-process)))

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


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.

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

(use-package ivy
  :diminish
  :bind (:map ivy-minibuffer-map
         ("C-m" . ivy-alt-done))
  :init
  (ivy-mode 1)
  :config
  (setq ivy-use-virtual-buffers t)
  (setq ivy-wrap t)
  (setq ivy-count-format "(%d/%d) ")
  (setq enable-recursive-minibuffers t)
  (setq projectile-completion-system 'ivy) ; Let projectile use ivy

  ;; Use different regex strategies per completion command
  (push '(completion-at-point . ivy--regex-fuzzy) ivy-re-builders-alist) ;; This doesn't seem to work...
  (push '(counsel-M-x . ivy--regex-ignore-order) ivy-re-builders-alist)

  ;; Set minibuffer height for different commands
  (setf (alist-get 'counsel-projectile-ag ivy-height-alist) 15)
  (setf (alist-get 'counsel-projectile-rg ivy-height-alist) 15)
  (setf (alist-get 'counsel-switch-buffer ivy-height-alist) 7))

(use-package ivy-hydra
  :defer t
  :after hydra)

(use-package ivy-rich
  :init
  (ivy-rich-mode 1)
  :after counsel
  :config
  (setq ivy-format-function #'ivy-format-function-line)
  (setq ivy-rich-display-transformers-list
        (plist-put ivy-rich-display-transformers-list
                   'ivy-switch-buffer
                   '(:columns
                     ((ivy-rich-candidate (:width 40))
                      (ivy-rich-switch-buffer-indicators (:width 4 :face error :align right)); return the buffer indicators
                      (ivy-rich-switch-buffer-major-mode (:width 12 :face warning))          ; return the major mode info
                      (ivy-rich-switch-buffer-project (:width 15 :face success))             ; return project name using `projectile'
                      (ivy-rich-switch-buffer-path (:width (lambda (x) (ivy-rich-switch-buffer-shorten-path x (ivy-rich-minibuffer-width 0.3))))))  ; return file path relative to project root or `default-directory' if project is nil
                     :predicate
                     (lambda (cand)
                       (if-let ((buffer (get-buffer cand)))
                           ;; Don't mess with EXWM buffers
                           (with-current-buffer buffer
                             (not (derived-mode-p 'exwm-mode)))))))))

(use-package counsel
  :demand t
  :bind (("M-x" . counsel-M-x)
         ("C-x b" . counsel-ibuffer)
         ("C-x C-f" . counsel-find-file)
         ;; ("C-M-j" . counsel-switch-buffer)
         ("C-M-l" . counsel-imenu)
         :map minibuffer-local-map
         ("C-r" . 'counsel-minibuffer-history))
  :custom
  (counsel-linux-app-format-function #'counsel-linux-app-format-function-name-only)
  :config
  (setq ivy-initial-inputs-alist nil)) ;; Don't start searches with ^

(use-package flx  ;; Improves sorting for fuzzy-matched results
  :after ivy
  :defer t
  :init
  (setq ivy-flx-limit 10000)
  (setq ivy-re-builders-alist '((t . ivy--regex-fuzzy))))


;; (use-package ivy-posframe

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

;;   :custom
;;   (ivy-posframe-width      115)
;;   (ivy-posframe-min-width  115)
;;   (ivy-posframe-height     10)
;;   (ivy-posframe-min-height 10)
;;   :config
;;   (setq ivy-posframe-display-functions-alist '((t . ivy-posframe-display-at-frame-top-center)))
;;   (setq ivy-posframe-parameters '((parent-frame . nil)
;;                                   (left-fringe . 8)
;;                                   (right-fringe . 8)))
;;   (ivy-posframe-mode 1))

(use-package prescient
  :after counsel
  :config
  (prescient-persist-mode 1))

(use-package ivy-prescient
  :after prescient
  :config
  (ivy-prescient-mode 1))

Compilation modes

I use compilation-mode often when running quick tests for a module or similar. Set it up so it's nicer to work with.

(use-package compile
  :config
  (setq compilation-scroll-output 'first-error))

(defun auto-recompile-buffer ()
  (interactive)
  (if (member #'recompile after-save-hook)
      (remove-hook 'after-save-hook #'recompile t)
    (add-hook 'after-save-hook #'recompile nil t)))

;; colorize the output of the compilation mode.
(use-package 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)

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

(with-eval-after-load 'esh-opt
  (setq eshell-destroy-buffer-when-process-dies t)
  (setq eshell-visual-commands '("htop" "zsh" "vim")))


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

  ;; improve colors in terminals
  (use-package eterm-256color
    :hook (term-mode))

  (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"))
    :config '(
              (setq vterm-always-compile-module t)
              (setq vterm-term-environment-variable "eterm-256color")
              (setq vterm-kill-buffer-on-exit t)))

(use-package eshell-syntax-highlighting
  :after esh-mode
  :config
  (eshell-syntax-highlighting-global-mode +1))

Blogging

I'm slowly porting over to using org-static-blog as it has some blog-centric output which is quite helpful.

(use-package org-static-blog
  :after org
  :config (load "~/.emacs.d/static-blog.el"))

I keep the static blog separate so it's easier to load just this config for publishing.

(package-initialize)
(require 'org-static-blog)

(setq org-static-blog-publish-title "Justin Abrahms"
      org-static-blog-publish-url "https://justin.abrah.ms/"
      org-static-blog-publish-directory "/srv/justin.abrah.ms"
      org-static-blog-rss-file "/rss.xml"
      ;; Note: these can't be dynamic b/c you can't take repo-relative b/c it gets compiled out to ~/.emacs.d
      org-static-blog-posts-directory  "~/src/justin.abrah.ms/blog/_posts"
      org-static-blog-drafts-directory "~/src/justin.abrah.ms/blog/_posts"
      org-static-blog-enable-tags t
      org-export-with-toc nil
      org-export-with-section-numbers nil
      org-static-blog-page-preamble (concat "<nav><a href=\"" org-static-blog-publish-url "\">Home</a></nav>
<nav class=\"fontawesome\">
    <a href=\"https://twitter.com/justinabrahms\" target=\"_blank\">
        <i title=\"Twitter\" class=\"fab fa-twitter\"></i>
    </a>
    <a href=\"mailto:justin@abrah.ms\" target=\"_blank\">
        <i title=\"Email\" class=\"fas fa-at\"></i>
    </a>
    <a href=\"" org-static-blog-rss-file "\" target=\"_blank\">
        <i title=\"RSS\" class=\"fas fa-rss\"></i>
    </a>
  </nav>
")
      org-static-blog-page-header "
<style type=\"text/css\">
@media (min-width: 50em) {
  @font-face {
    font-family: 'PT Sans';
    src: local(\"PT Sans\"), local(\"PTSans-Regular\"), url(\"../fonts/PTSans-Regular.woff\") format(\"woff\");
    font-weight: normal;
    font-style: normal;
  }
  @font-face {
    font-family: 'PT Sans';
    src: local(\"PT Sans Bold\"), local(\"PTSans-Bold\"), url(\"../fonts/PTSans-Bold.woff\") format(\"woff\");
    font-weight: bold;
    font-style: normal;
  }
}

@media (prefers-color-scheme: light) {
  html {
    background: #ffffff;
    color: #1c1d22;
  }

  #content a {
    color: #0000EE;
  }

  .foot a:hover {
    text-decoration: underline;
    color: #0000EE;
  }
}

@media (prefers-color-scheme: dark) {
  html {
    background: #1c1d22;
    color: #ffffff;
  }

  article a, #content a {
    color: #58A6FF;
  }

  .foot a:hover {
    text-decoration: underline;
    color: #58A6FF;
  }
}

html {
  font-size: 16px;
  font-size: calc(0.8rem + 0.3vw);
}

body {
  font-family: \"PT Sans\", -apple-system, BlinkMacSystemFont, \"Helvetica Neue\", \"Segoe UI\", \"Roboto\", sans-serif;
  font-weight: 400;
  line-height: 1.5;
  margin: 0;
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, h4, h5, h6 {
  font-weight: 600;
}

b, strong, th {
  font-weight: 600;
}

a {
  color: inherit;
  text-decoration: inherit;
}

a:hover {
  text-decoration: underline;
}

header time, .post-date, .taglist {
  color: #8e8e91;
}

.taglist {
  display: inline;
}

.taglist > .fa-folder {
  padding: 0 10px;
}

hr {
  border: 1px solid rgba(142, 142, 145, 0.3);
  margin: 2em 0;
}

article:not(:last-child) {
  border-bottom: 1px solid rgba(142, 142, 145, 0.12);
}

blockquote {
  background: rgba(142, 142, 145, 0.06);
  border-left: 3px solid rgba(142, 142, 145, 0.9);
  padding: 1px 1.5em;
  /* opacity: .75; */
}

blockquote, figure {
  margin: 1em 0;
}

img, li {
  margin: .5em 0;
}

img {
  border-radius: 5px;
  max-width: 100%;
  height: auto;
  /* center horizontally */
  margin-left: auto;
  margin-right: auto;
  display: block;
}

table {
  width: 100%;
  border-spacing: 1px;
  box-shadow: 0 0 0 1px rgba(142, 142, 145, 0.12) inset;
}

th, td {
  padding: .5em 1em;
  box-shadow: 0 0 0 1px rgba(142, 142, 145, 0.12);
}

tr:hover, tr:nth-child(odd) td {
  background: rgba(142, 142, 145, 0.04);
}

pre {
  background: rgba(142, 142, 145, 0.12);
  border-radius: 2px;
  font-size: .8em;
  /* font-family: fira mono,cousine,Monaco,Menlo,source code pro,monospace; */
  font-family: 'Courier New', Courier, monospace;
  margin: 1.5em 0;
  padding: .8em 1.2em;
  overflow-x: auto;
}

:not(pre) > code {
  font-size: .9em;
  background: rgba(142, 142, 145, 0.15);
  /* opacity: .7; */
  border-radius: 2px;
  margin: 0 .1em;
  padding: .2em .4em;
}

body > header, body > #preamble {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  background: rgba(255, 255, 255, 0.02);
  box-shadow: 0 0 .6em rgba(28, 29, 34, 0.05);
  border-bottom: 1px solid rgba(142, 142, 145, 0.16);
}

body > header > a, body > #preamble > a {
  font-size: 1.3em;
}

article header, #content #preamble {
  margin-bottom: 1.5em;
}

article header h1, #content #preamble h1 {
  font-size: 1.8em;
  margin: .5em .5em;
}

article h2, #content h2 {
  padding-bottom: 0.3em;
  font-size: 1.5em;
  border-bottom: 1px solid #eaecef;
}

nav {
  margin: .5em -.8em;
}

nav a {
  margin: .5em .8em;
}

footer hr { /* custom */
  width: 45%;
  border-style: dashed;
  margin-bottom: 1em;
}

body > header, body > #preamble, body > article, body>#content, body > footer {
  padding: 1.5em;
}

@media (min-width: 32em) {
  body > header, body > #preamble, body > article, body > #content, body > footer {
    padding: 1.75em calc(38% - 12em);
  }
}

.archive li {
  font-size: 1.1em;
  list-style: none;
}

.archive time, .archive post-date {
  display: inline-block;
  min-width: 10ch;
  margin: 0 .2em;
}

.hidden {
  display: none;
}

.more {
  margin: 2em 0 1em;
}

.more a {
  border-radius: 2px;
  border: 1.5px solid #68f;
  padding: .4em .8em;
  transition: .2s;
}

.more a:hover {
  color: #fff;
  background: #68f;
  text-decoration: inherit;
}

/***** custom *****/
.fontawesome a {
  text-decoration: none;
}

.post-meta {
  color: #8e8e91;
}

.comments-item {
  margin-top: 10%;
}

.foot {
  font-family: \"PT Sans\", -apple-system, BlinkMacSystemFont, \"Helvetica Neue\", \"Segoe UI\", \"Roboto\", sans-serif;
  margin: 1%;
  color: #8e8e91;
  text-align: center;
}

</style>

<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta charset='utf-8'>
<link rel='alternate' type='application/rss+xml' href='/rss.xml' title='Justin Abrahms'>
<link href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css' rel='stylesheet'>
"
      org-static-blog-index-file "no-one-cares"
      org-static-blog-archive-file "index.html"
      org-static-blog-page-postamble (concat
                                      " <div class='foot'>
    &copy; 2012 - 2022 &#183;
    <a href=\"" org-static-blog-publish-url "\" >Home</a> &mdash; Theme <a href='https://github.com/RainerChiang/simpleness'>Simpleness</a>
    <a href='#'><i class='fas fa-chevron-up'></i></a>
</div>"))

(defun org-static-blog-assemble-archive ()
  "Re-render the blog archive page.
The archive page contains single-line links and dates for every
blog post, but no post body."
  (let ((archive-filename (concat-to-dir org-static-blog-publish-directory org-static-blog-archive-file))
        (archive-entries nil)
        (post-filenames (org-static-blog-get-post-filenames)))
    (setq post-filenames (sort post-filenames (lambda (x y) (time-less-p
                                                             (org-static-blog-get-date y)
                                                             (org-static-blog-get-date x)))))
    (org-static-blog-with-find-file
     archive-filename
     (org-static-blog-template
      org-static-blog-publish-title
      (concat
       "<ul class=\"archive\">"
       (apply 'concat (mapcar (lambda (post-filename)
                                (concat
                                 "<li>"
                                 "<time>" (format-time-string (org-static-blog-gettext 'date-format) (org-static-blog-get-date post-filename))"</time>"
                                 "<a href=\"" (org-static-blog-get-post-url post-filename)  "\">"
                                 (org-static-blog-get-title post-filename)
                                 "</a>"
                                 "</li>")
                                ) post-filenames))
       "</div>")))))

(defun org-static-blog-post-preamble (post-filename)
  "Returns the formatted date and headline of the post.
This function is called for every post and prepended to the post body.
Modify this function if you want to change a posts headline."
  (concat
   "<h1 class=\"post-title\">"
   (org-static-blog-get-title post-filename)
   "</h1>\n"
   "<div class=\"post-meta\"><time>"
   (format-time-string (org-static-blog-gettext 'date-format)
                       (org-static-blog-get-date post-filename))
   "</time>"
   "<div class=\"taglist\">"
   "<i class=\"fas fa-folder\"></i>"
   (org-static-blog-post-taglist post-filename)
   "</div>"
   "</div>"))

(defun org-static-blog-post-postamble (post-filename)
  "Returns the tag list and comment box at the end of a post.
This function is called for every post and the returned string is
appended to the post body, and includes the tag list generated by
followed by the HTML code for comments."
  (concat
   (if (string= org-static-blog-post-comments "")
       ""
     (concat "\n<div id=\"comments\">"
             org-static-blog-post-comments
             "</div>"))))

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.

Email

I've been using mu4e for both work and (more recently) personal email. These configs work with both exchange and my personal imap server.

mbsync

I've been using offlineimap for a while, but it suffers from two problems. First, it's a bit slow. Not critical, but a sticking point. More importantly, it's difficult to separate which things do or don't sync based on my particular hosts. I have work email and personal email. I'd love to have the same config and have my work email sync at work and personal email sync at home. My initial config was inspired by copyninja.

# Docs at https://isync.sourceforge.io/mbsync.html
IMAPAccount fastmail
Host imap.fastmail.com
User justin@abrah.ms
PassCmd "gpg -q --for-your-eyes-only --no-tty --exit-on-status-write-error --batch -d ~/.fastmail.password.gpg"
SSLType IMAPS
SSLVersion TLSv1.2
CertificateFile /etc/ssl/certs/ca-certificates.crt

IMAPAccount walmart
Host outlook.wal-mart.com
User t0a01lc@homeoffice.wal-mart.com
PassCmd "gpg -q --for-your-eyes-only --no-tty --exit-on-status-write-error --batch --passphrase-file ~/.gpg-pass.txt -d ~/.walmart.password.gpg"
SSLType IMAPS
SSLVersion TLSv1.2
CertificateFile /etc/ssl/certs/ca-certificates.crt


IMAPStore fastmail-remote
Account fastmail

MaildirStore fastmail-local
SubFolders Verbatim
Path ~/Mail/fastmail/
Inbox ~/Mail/fastmail/INBOX

IMAPStore walmart-remote
Account walmart

MaildirStore walmart-local
SubFolders Verbatim
Path ~/Mail/Walmart/
Inbox ~/Mail/Walmart/INBOX

Channel fastmail
Master :fastmail-remote:
Slave :fastmail-local:
Patterns * !migrated* !Spam
Create Both
SyncState *
Sync All

Channel walmart
Master :walmart-remote:
Slave :walmart-local:
# Exclude everything under the internal [Gmail] folder, except the interesting folders
Patterns * !*Calendar* !Contacts !&XvqLrnaEgFR8+066-
Create Both

SyncState *
Sync All

Mu4e config

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

(let ((mu4e-dir (concat (getenv "HOME") "/src/github.com/djcb/mu/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 'mu4e-org)

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

        ;; Without this changing of filenames, we get errors w/ UID
        ;; synchronization. Not vetted yet, but
        ;; https://stackoverflow.com/q/39513469/4972 is a breadcrumb.
        (setq mu4e-change-filenames-when-moving t)

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

        ;; via https://emacs.stackexchange.com/questions/51999/muting-threads-in-mu4e
        (add-to-list 'mu4e-headers-actions
                     '("Mute thread" .
                       (lambda (msg)
                         (let ((docid (mu4e-message-field msg :docid))
                               (refloc (if (stringp mu4e-refile-folder)
                                           mu4e-refile-folder
                                         (funcall mu4e-refile-folder msg))))
                           (mu4e-action-retag-message msg "+muted")
                           (mu4e~proc-move docid refloc "+S-N")))))

        (defun mu4e-archive-if-muted ()
          (let* ((cmd "mu find tag:muted --format=sexp -r")
                 (result (concat "(list " (shell-command-to-string cmd) ")"))
                 (msgs (car (read-from-string result))))
            (dolist (msg (cdr msgs))
              (let ((maildir (mu4e-message-field msg :maildir))
                    (docid (mu4e-message-field msg :docid))
                    (refloc (if (stringp mu4e-refile-folder)
                                mu4e-refile-folder
                              (funcall mu4e-refile-folder msg))))
                (unless (or (string= refloc maildir)
                            (string= "/sent" maildir))
                  (when (and docid (> docid 0))
                    (mu4e~proc-move docid refloc "+S-N")))))))

        (add-to-list 'mu4e-index-updated-hook 'mu4e-archive-if-muted t)

        (setq mu4e-bookmarks
              '(("(maildir:/Walmart/INBOX OR maildir:/fastmail/INBOX) AND NOT flag:trashed" "Inbox" ?i)
                ("maildir:/fastmail/lists* NOT flag:trashed" "Mailing Lists" ?l)
                ("maildir:/Walmart/github AND NOT flag:trashed AND flag:unread" "Code Review" ?g)
                ("maildir:/Walmart/jira AND NOT flag:trashed AND flag:unread" "Tickets" ?j)
                ))

        (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)
                                        )
                                )
                              ,(make-mu4e-context
                                :name "Fastmail"
                                ;; enter-func not defined
                                ;; leave-func not defined
                                ;; match-func not defined
                                :vars '((user-mail-address . "justin@abrah.ms")
                                        (mu4e-sent-folder . "/fastmail/Sent")
                                        (mu4e-drafts-folder . "/fastmail/Drafts")
                                        (mu4e-trash-folder . "/fastmail/Trash")
                                        (mu4e-refile-folder . "/fastmail/Archive")
                                        (smtpmail-default-smtp-server . "smtp.fastmail.com")
                                        (smtpmail-local-domain . "abrah.ms")
                                        (smtpmail-smtp-user . "justin@abrah.ms")
                                        (smtpmail-smtp-server . "smtp.fastmail.com")
                                        (smtpmail-stream-type . starttls)
                                        (smtpmail-smtp-service . 587)))



                              ))

        ;; 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;
options.info = false

function hostname()
   local f = io.popen ("/bin/hostname")
   local hostname = f:read("*a") or ""
   f:close()
   hostname =string.gsub(hostname, "\n$", "")
   return hostname
end


function main()
   local host = hostname()
   if host == "periwinkle" then
      walmart()
   elseif host == "justin-x250" then
      fastmail()
   else
      print("Unknown host. Unsure which imap servers to contact")
   end
end

function charities(account, mbox)
      dst = account["charities"]
      mbox:contain_from("Mozilla@e.mozilla.org"):contain_subject("Receipt"):move_messages(dst) -- mozilla
      mbox:contain_subject("Recurring Contribution"):move_messages(dst) -- eff
      mbox:contain_subject('automatic payment to Wikimedia'):move_messages(dst) -- wikipedia
end

function fastmail()
   local fastmail = IMAP {
      server = "imap.fastmail.com",
      username = "justin@abrah.ms",
      password = read_encrypted_contents("~/.fastmail.password.gpg"),
      ssl = 'ssl3',
   }
   fastmail.INBOX:check_status()
   mails = fastmail.INBOX:select_all()

   delete_mail_if_from(mails, 'grubhub')
   delete_mail_if_from(mails, 'aetna')

   -- linkedin spam
   delete_mail_if_subject_contains(mails, 'add me to your LinkedIn network')
   delete_mail_if_subject_contains(mails, 'you\'re getting noticed')
   delete_mail_if_subject_contains(mails, 'invitation is waiting for your response')

   delete_mail_if_subject_contains(mails, 'sent a message to your group conversation') -- fb messenger
   delete_mail_if_subject_contains(mails, 'Terms of Service')
   delete_mail_if_subject_contains(mails, 'Change in Terms')
   delete_mail_if_subject_contains(mails, 'Your Statement of Account from')
   delete_mail_if_subject_contains(mails, 'has sent you a Direct Message on Twitter')
   delete_mail_if_subject_contains(mails, 'Shared Moments from Escuela-Viva')

   -- receipts
   mails:match_subject('Your amazon.com order .* has shipped'):delete_messages()
   mails:match_subject('Your Electronic Receipt'):move_messages(fastmail["receipts"])
   mails:match_subject('Your Amazon.com order #'):move_messages(fastmail["receipts"])
   mails:match_subject('Mozilla VPN payment received'):move_messages(fastmail["receipts"])
   mails:match_subject('DNSImple Invoice'):move_messages(fastmail["receipts"])
   mails:match_subject('Billing Statement Available'):move_messages(fastmail["receipts"]) -- aws
   mails:match_subject('invoice is available'):move_messages(fastmail["receipts"]) -- digital ocean
   mails:match_subject('Statement Notification'):move_messages(fastmail["receipts"]) -- trash
   mails:match_subject('Your.*Reciept'):move_messages(fastmail["receipts"]) -- google play
   mails:match_subject('Thank you .* contribution'):move_messages(fastmail["receipts"]) -- open collective
   mails:contain_from('venmo@'):move_messages(fastmail["receipts"]) 

   charities(fastmail, mails)
   automatic_mailing_list_folders(fastmail, mails)
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(mails, 'sneal@')
   delete_mail_if_to(mails, 'WalmartPayMetricsEma@')

   mails:contain_subject("Honesty Market"):delete_messages()
   mails:contain_subject("onboarded to Hygieia"):delete_messages()

   mails:contain_from('svc_automaton@walmartlabs.com'):move_messages(walmart['load-tests'])
   mails:contain_subject('Your meeting attendees are waiting'):delete_messages() -- zoom
   mails:contain_subject('has joined your Personal Meeting Room'):delete_messages() -- zoom

   mails:contain_subject('You have been onboarded to Hygieia'):delete_messages()


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

   -- Journey mapping emails to a folder
   mails:contain_subject("FS Applications Journey Mapping"):move_messages(walmart['journey-mapping'])

   -- alerts that I don't care about
   mails:contain_from('support@oneops.com'):move_messages(walmart['ops-alerts'])

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

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

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

function delete_mail_if_to(mails, to)
   filtered = mails:contain_to(to)
   filtered:delete_messages()
end


function read_encrypted_contents(filename)
   local handle = io.popen("gpg -q --for-your-eyes-only --no-tty --exit-on-status-write-error --batch --passphrase-file ~/.gpg-pass.txt -d ".. filename)
   local result = handle:read("*a")
   handle:close()
   return result
end

-- Utility function to get IMAP password from a gpg encrypted file
function read_encrypted(key, value)
   local result = read_encrypted_contents("~/.offlineimappass.gpg")
   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

function automatic_mailing_list_folders(account, mbox)
      -- Thanks to KangOI at https://gist.github.com/KangOl/4533912
   messages = mbox:contain_field('List-Id', '')
   ML = "lists/"
   AUTO_ML = ML.."Auto/"
   mls = {}

   for _, message in ipairs(messages) do
      mailbox, uid = table.unpack(message)
      listid = mailbox[uid]:fetch_field('List-Id')
      subj = mailbox[uid]:fetch_field('subject')
      if bool(listid) then
         -- imapfilter bug: the search is also done inside the message body
         -- and thus also match the forwarded emails.
         s, m1, m2 = regex_search('^(?i)List-Id: (.*) <(.*)>$', listid)
         if s and bool(m1) and bool(m2) then
            mls[m2] = m1:gsub('"', '')
         end
      end
   end

   for listid, listtitle in pairs(mls) do
      dest = AUTO_ML..listtitle
      account:create_mailbox(dest)
      account:subscribe_mailbox(dest)
      messages:contain_field('List-Id', listid):move_messages(account[dest])
   end
end

bool = function(o)
    if not o then
        return false
    elseif o == '' then
        return false
    elseif o == 0 then
        return false
    elseif #o == 0 then
        return false
    end
    return true
end

pyor = function(o)
    if not bool(o) then
        return false
    end
    return o
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

Projectile

Projectile allows for navigating to files that are within the same "project". The "project" is ~a git repository.

(use-package projectile)

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.

(use-package 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.

(use-package 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.

(use-package scpaste
  :config
  (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.

(use-package yasnippet
  :config
  (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)
(use-package git-link) ;; link to a given line of code you're on

Emoji

Having emoji is nice.

(use-package emojify
  :hook (org-mode . emojify-mode)
  :commands emojify-mode)

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)
(define-key projectile-mode-map (kbd "C-c p") 'projectile-command-map)

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

(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
  :defer t
  :hook ((lsp-mode . flycheck-mode)
         (sh-mode . flycheck-mode)
         (go-mode . flycheck-mode)
         (python-mode . flycheck-mode)
         (markdown-mode . flycheck-mode)
         (js2-mode . flycheck-mode))
  :ensure-system-package ((proselint . "pip3 install --user proselint") shellcheck))

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

(use-package plan9-theme)

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

Package maintenance

I'm beginning to make small elisp packages and these packages help me ensure that they're of high quality.

(use-package package-lint)

elpher

I've been playing with gemini (and to a lesser extent, the gopher protocol), an alternative to the web. It's got a ton of great content on it. It's useful to have a client to browse it.

(use-package elpher)

Diagramming

PlantUML is also a good diagramming tool. Add support for that.

(use-package plantuml-mode
  :config (setq plantuml-default-exec-mode "jar"
                plantuml-jar-path (expand-file-name "~/plantuml-1.2021.15.jar")
                org-plantuml-jar-path plantuml-jar-path)
  (add-to-list 'org-src-lang-modes '("plantuml" . plantuml))
  (org-babel-do-load-languages 'org-babel-load-languages '((plantuml . t)))
  :ensure-system-package
  ("~/plantuml-1.2021.15.jar" . "curl -L -o ~/plantuml-1.2021.15.jar https://github.com/plantuml/plantuml/releases/download/v1.2021.15/plantuml-1.2021.15.jar"))

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)

;; This is a terrible idea and makes everything wrap poorly, including source code.
;; (global-visual-fill-column-mode) ;; always enable visual-fill-mode
(use-package visual-fill-column) ;; wrap text nicely.


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


;; auto-revert Dired and other buffers
(require 'dired-x)
(add-hook 'dired-mode-hook (lambda ()
                             (dired-omit-mode 1)))
(setq global-auto-revert-non-file-buffers t)

(use-package dired-single) ; keep dired stuff in a single window instead of a bunch of them


;; Fantastic dired thing which will hide most `ls -l` output.
;; ) and ( to toggle it
;; (require 'dired-details)
;; (dired-details-install)

;; scala

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


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


Output the source of mscgen-mode which isn't loadble anywhere else via melpa. Source.

;;; mscgen-mode.el --- Major mode for editing mscgen sequence diagrams

;; Copyright (C) 2018 Thomas Stenersen

;; Author: Thomas Stenersen <stenersen.thomas@gmail.com>
;; Version: 0.1

;; This file is not part of GNU Emacs.

;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; Major mode for editing mscgen sequence diagrams

;;; Code:


(defgroup mscgen nil "mscgen customizations"
  :group 'languages)

(defcustom mscgen-executable
  "mscgen"
  "Path to `mscgen' exectuable."
  :type 'string
  :group 'mscgen)

(defcustom mscgen-output-file-type
  "png"
  "Default output file type."
  :group 'mscgen
  :type 'string
  :options '("png" "eps" "svg" "ismap"))

;;; Fontification

(defconst mscgen--keywords
  '("label" "URL" "ID" "IDURL" "arcskip" "linecolor" "textcolor" "textbgcolor"
    "arclinecolor" "arctextcolor" "arctextbgcolor"))

(defconst mscgen--keywords-extra
  '("linecolour" "textcolour" "textbgcolour" "arclinecolour" "arctextcolour"
    "arctextbgcolour"))

(defconst mscgen--types
  '("box" "note" "rbox" "abox" "->" "<-" "=>" "<=" ">>" "<<"
    "=>>" "<<=" ":>" "<:" "-x" "x-" "*<-" "->*" "..." "---"
    "|||"))

(defconst mscgen--functions
  '("arcgradient" "wordwraparcs" "width" "hscale"))

(defconst mscgen--font-lock-keywords
  (let* ((x-keywords-regexp (regexp-opt (append mscgen--keywords
                                                mscgen--keywords-extra)))
         (x-types-regexp (regexp-opt mscgen--types) )
         (x-functions-regexp (regexp-opt mscgen--functions)))
    `((,x-keywords-regexp . font-lock-keyword-face)
      (,x-types-regexp . font-lock-type-face)
      (,x-functions-regexp . font-lock-function-name-face))))

(defconst mscgen-syntax-table
  (let ( (syn-table (make-syntax-table)))
    ;; python style comment: # &
    (modify-syntax-entry ?# "<" syn-table)
    (modify-syntax-entry ?\n ">" syn-table)
    syn-table)
  "Syntax table for mscgen comments.")


;;; Functions

(defun mscgen-completion-at-point ()
  "Complete the label or function at the current point."
  (interactive)
  (let* ((bds (bounds-of-thing-at-point 'symbol))
         (start (car bds))
         (end (cdr bds)))
    (list start end (append mscgen--keywords
                            mscgen--types
                            mscgen--functions) . nil)))

(defun mscgen-compile ()
  "Compile the current sequence diagram."
  (interactive)
  (let ((compile-command
         (read-from-minibuffer
          "Compile command: "
          (format
           "%s -T %s -i %s -o %s.%s"
           mscgen-executable
           mscgen-output-file-type
           (buffer-file-name)
           (file-name-sans-extension (buffer-file-name))
           mscgen-output-file-type))))
    (compile compile-command)))

(defun mscgen-insert-label-at-point ()
  "Quick insert a label at point."
  (interactive)
  (end-of-line)
  (insert (format "[label=\"%s\"];" (read-from-minibuffer "Label: "))))

(define-derived-mode mscgen-mode fundamental-mode "mscgen-mode"
  "Major mode for editing mscgen sequence diagrams. See
  http://www.mcternan.me.uk/mscgen/."

  (setq font-lock-defaults '((mscgen--font-lock-keywords)))
  (set-syntax-table mscgen-syntax-table)
  (setq-local comment-start "#")
  (setq-local comment-end "")
  (add-hook 'completion-at-point-functions
            'mscgen-completion-at-point nil 'local))

(provide 'mscgen-mode)


;;; mscgen-mode.el ends here

Finishing up

We need to reduce the GC threshold so we have small fast GCs for normal operation.

;; Make gc pauses faster by decreasing the threshold.
(setq gc-cons-threshold (* 2 1000 1000))

Inspiration

"Inspiration", in large part, means I blatantly stole from them. I started this list at ~3k lines of literate config, so I've missed folks.

© 2012 - 2022 · Home — Theme Simpleness