dotfiles/emacs/init.org
2020-06-14 12:28:04 -04:00

221 KiB
Executable File

-- eval: (add-hook 'after-save-hook 'org-babel-tangle 0 t) --

This is a literate init file holding my Emacs configuration. It is initially loaded by a bootstrap file that lives at ~/.emacs.d/init.el; after the initial bootstrapping it writes itself to ~/.emacs.el. Since ~/.emacs.el takes priority over ~/.emacs.d/init.el, after the initial bootstrapping process the tangled ~/.emacs.el file will get loaded without needing to load the bootstrap file first.

Prelude

Enables lexical binding for everything in init.el:

  ;;;  -*- lexical-binding: t; -*-

Garbage collection

Some GC tweaks "borrowed" from Doom emacs.

Turn off GC during init and restore it afterwards:

  (setq gc-cons-threshold most-positive-fixnum
	gc-cons-percentage 0.6)

  (add-hook 'emacs-startup-hook
      (lambda ()
	(setq gc-cons-threshold 100000000
	      gc-cons-percentage 0.1)))

Also suppress GC for 1 second after the minibuffer is active to avoid stuttering autocompletion and other GC hangups:

  (defun defer-garbage-collection ()
    (setq gc-cons-threshold most-positive-fixnum))

  (defun restore-garbage-collection ()
    (run-at-time
     1 nil (lambda () (setq gc-cons-threshold 100000000))))

  (add-hook 'minibuffer-setup-hook #'defer-garbage-collection)
  (add-hook 'minibuffer-exit-hook #'restore-garbage-collection)

Unset file-handler-alist during initialization

Another optimization from Doom Emacs.

  (defvar file-name-handler-alist-backup file-name-handler-alist)
  (setq file-name-handler-alist nil)
  (add-hook 'emacs-startup-hook
	    (lambda ()
	      (setq file-name-handler-alist file-name-handler-alist-backup)))

Variables

  (setq vc-follow-symlinks t
        frame-resize-pixelwise t
        tab-always-indent 'complete
        enable-recursive-minibuffers t
        read-process-output-max (* 1024 1024))
  (setq-default indent-tabs-mode nil)

Default directory

  (cd "~")

Packages

Load straight.el to manage package installation:

  (defvar bootstrap-version)
  (unless (boundp 'bootstrapping-init)
   (let ((bootstrap-file
	 (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
	(bootstrap-version 5))
    (unless (file-exists-p bootstrap-file)
      (with-current-buffer
	  (url-retrieve-synchronously
	   "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
	   'silent 'inhibit-cookies)
	(goto-char (point-max))
	(eval-print-last-sexp)))
    (load bootstrap-file nil 'nomessage)))

`use-package` is a macro that simplifies installing and loading packages.

  (straight-use-package 'use-package)
  (setq straight-use-package-by-default t)

Benchmarking

`benchmark-init` does what it says on the box. This sets it up to benchmark my init time and then disable benchmarking after init completes.

  (use-package benchmark-init
    :config
    (add-hook 'after-init-hook 'benchmark-init/deactivate))

Load org mode

Load org-mode early to avoid a version clash.

  (use-package org
    :straight org-plus-contrib
    :commands (org-element-map)
    :mode (("\\.org\\'" . org-mode)))

  ;; Annoying that this is necessary...
  (require 'org)
  (require 'org-refile)
  (require 'org-protocol)

Doom themes

  (use-package doom-themes)

  (use-package doom-modeline)
  (doom-modeline-mode 1)

Ewal theme

  (use-package doom-ewal-themes
    :straight (doom-ewal-themes
	       :host github
	       :repo "jdormit/doom-ewal-themes"
	       :files ("themes" :defaults)))

Customization File

I don't want anything to write to my init.el, so save customizations in a separate file:

  (setq custom-file (expand-file-name "custom.el" user-emacs-directory))
  (load custom-file t)

Path

`exec-path-from-shell` uses Bash to set MANPATH, PATH, and exec-path from those defined in the user's shell config. This won't work on Windows.

  (use-package exec-path-from-shell
    :if (memq window-system '(mac ns x))
    :config
    (setq exec-path-from-shell-variables '("PATH" "MANPATH" "LEDGER_FILE" "LOLA_HOME"
                                           "MODELS_HOME" "LOLA_TRAVEL_SERVICE_HOME" "WORKON_HOME"
                                           "PIPENV_VERBOSITY" "PIPENV_DONT_LOAD_ENV"
                                           "PIPENV_MAX_DEPTH" "PYENV_ROOT")
          exec-path-from-shell-check-startup-files nil)
    (exec-path-from-shell-initialize))

General

Better keybinding.

  (use-package general
    :init
    (setq general-override-states '(insert
				    emacs
				    hybrid
				    normal
				    visual
				    motion
				    operator
				    replace)))

Delete trailing whitespace on save:

  (defvar *should-delete-trailing-whitespace* t)
  (defun delete-trailing-whitespace-hook-fn ()
    (when *should-delete-trailing-whitespace*
      (delete-trailing-whitespace)))
  (add-hook 'before-save-hook #'delete-trailing-whitespace-hook-fn)

Autocompletion

There seems to be some contention about whether autocomplete or company are better autocomplete packages. I'm going with company for now because the maintainer seems nicer…

  (use-package company
    :config
    (setq company-idle-delay 0.3)
    (add-hook 'after-init-hook #'global-company-mode))

  (general-def "C-M-i" #'company-complete)
  (general-def "M-<tab>" #'company-complete)

Which-key

`which-key` makes keybindings discoverable.

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

This function defines a prefix group for `which-key` so that it doesn't display `prefix`.

  (defun jdormit/define-prefix (binding name)
    (which-key-add-key-based-replacements
      (concat leader " " binding)
      name)
    (which-key-add-key-based-replacements
      (concat "," " " binding)
      name))

Evil Mode

Because I like modal editing and dislike RSI.

  (use-package evil
    :init
    (setq evil-want-keybinding nil)
    :config
    (evil-mode 1))

Make undo not undo paragraphs at a time:

  (setq evil-want-fine-undo t)

Add a convenience function for making buffer-local ex-commands:

  (defun evil-ex-define-local-cmd (cmd function)
    (set (make-local-variable 'evil-ex-commands) (copy-tree evil-ex-commands))
    (evil-ex-define-cmd cmd function))

evil-collection

A collection of evil bindings for various modes

  (use-package evil-collection
    :after (evil)
    :config
    (setq evil-collection-company-use-tng nil)
    (evil-collection-init))

leader key

Use the spacebar as a leader key in evil-mode's normal state and in various other modes:

  (defconst leader "SPC")
  (general-define-key
   :keymaps 'override
   :states '(normal visual motion)
   "SPC" nil)
  (general-create-definer leader-def-key
    :keymaps 'override
    :states '(normal visual motion)
    :prefix leader
    :prefix-map 'leader-map)
  (jdormit/define-prefix "?" "help")
  (leader-def-key "?" help-map)

evil-snipe

  (use-package evil-snipe
    :after (evil)
    :config
    (evil-snipe-mode 1)
    (evil-snipe-override-mode 1)
    (add-to-list 'evil-snipe-disabled-modes 'dired-mode)
    (add-to-list 'evil-snipe-disabled-modes 'structlog-mode)
    (with-eval-after-load 'magit
      (add-hook 'magit-mode-hook 'turn-off-evil-snipe-override-mode))
    (with-eval-after-load 'prodigy
      (add-hook 'prodigy-mode-hook 'turn-off-evil-snipe-mode))
    (with-eval-after-load 'pass
      (add-hook 'pass-mode-hook 'turn-off-evil-snipe-mode)))

evil-commentary

Adds Evil commands to comment out lines of code:

  (use-package evil-commentary
    :after (evil)
    :hook ((prog-mode . evil-commentary-mode)))

Additional keybindings

  (general-def 'normal "zM" #'hs-hide-level :keymaps 'override)
  (general-def 'normal "z=" #'text-scale-increase :keymaps 'override)
  (general-def 'normal "z-" #'text-scale-decrease :keymaps 'override)
  (general-def 'normal "z0" #'text-scale-adjust :keymaps 'override)
  (general-def 'normal view-mode-map "0" nil :keymaps 'override)
  (general-def 'normal prodigy-view-mode-map "0" nil :keymaps 'override)
  (general-def 'normal messages-buffer-mode-map "SPC" leader-map :keymaps 'override)

Multiple cursors

  (use-package evil-mc
    :defer 0
    :config
    (global-evil-mc-mode)
    (general-def '(normal visual)
      "gs" evil-mc-cursors-map
      "M-n" #'evil-mc-make-and-goto-next-cursor
      "M-p" #'evil-mc-make-and-goto-prev-cursor
      "C-n" #'evil-mc-make-and-goto-next-match
      "C-t" #'evil-mc-skip-and-goto-next-match
      "C-p" #'evil-mc-make-and-goto-prev-match))
  (use-package evil-multiedit
    :defer 0
    :config
    (evil-multiedit-default-keybinds))

Transient

A framework for creating Magit-style popups:

  (use-package transient
    :commands (define-transient-command))

Hydra

Hydras are convenient keybinding menus.

  (use-package hydra
    :defer t)

Magit

Magit is objectively the best Git interface.

  (use-package magit
    :commands (magit-status
               magit-blame
               magit-find-file
               magit-name-local-branch))
  (jdormit/define-prefix "g" "git")
  (leader-def-key "gs" #'magit-status)
  (leader-def-key "gg" #'magit-file-dispatch)
  (leader-def-key "gf" #'magit-find-file)

Use ido-mode for completion within Magit:

;;  (setq magit-completing-read-function 'magit-ido-completing-read)

Forge

Forge is an extension for Magit that lets it interact with code forges (e.g. GitHub).

  (use-package forge
    :after (magit)
    :config
    (add-to-list 'forge-alist '("git.jeremydormitzer.com"
                                "git.jeremydormitzer.com/api/v1"
                                "git.jeremydormitzer.com"
                                forge-gitea-repository))
    (with-eval-after-load 'evil-magit
      (general-def '(normal motion) magit-mode-map
        "yu" #'forge-copy-url-at-point-as-kill))
    :general
    ((normal motion visual) forge-topic-list-mode-map
     "y" #'forge-copy-url-at-point-as-kill
     "q" #'quit-window)
    :custom
    (forge-owned-accounts '((jdormit . (remote-name "jdormit")))))

evil-magit

Evil keybindings for magit!

  (use-package evil-magit
    :after (evil magit)
    :config
    (with-eval-after-load 'magit
      (require 'evil-magit))
    :general
    ('normal magit-mode-map "SPC" leader-map))

Transient

  (setq transient-default-level 7)

git-link

Open files in Git forges (GitHub, GitLab, etc.):

  (use-package git-link
    :init
    (defun git-link-copy ()
      (interactive)
      (let ((git-link-open-in-browser nil))
        (call-interactively 'git-link)))
    (leader-def-key "gl" #'git-link)
    (leader-def-key "gy" #'git-link-copy)
    :custom
    (git-link-open-in-browser t)
    (git-link-remote-alist '(("git.sr.ht" git-link-sourcehut)
                             ("github" git-link-github)
                             ("bitbucket" git-link-bitbucket)
                             ("gitorious" git-link-gitorious)
                             ("gitlab" git-link-gitlab)
                             ("visualstudio\\|azure" git-link-azure)
                             ("git.jeremydormitzer.com" git-link-bitbucket))))

with-editor

A utility from the author of Magit to run shell commands using the current Emacs instance as $EDITOR.

  (shell-command-with-editor-mode)
  (add-hook 'shell-mode-hook #'with-editor-export-editor)
  (add-hook 'term-exec-hook #'with-editor-export-editor)
  (add-hook 'eshell-mode-hook #'with-editor-export-editor)

Password Store

Interfacing with Pass, the "standard Unix password manager". This should also be loaded before `exec-path-from-shell`.

  (defun password-store-synchronize ()
    (interactive)
    (with-editor-async-shell-command "pass git pull && pass git push"))

  (use-package password-store
    :defer t
    :if (executable-find "pass")
    :config
    (setq password-store-password-length 20)
    :init
    (leader-def-key "P" 'password-store-copy)
    (epa-file-enable))

  (use-package pass
    :if (executable-find "pass")
    :commands pass
    :general
    ('(normal motion visual) pass-mode-map "S" #'password-store-synchronize))

  (leader-def-key "ap" #'pass)

  (setq auth-sources '("~/.authinfo" password-store))

Emacs Lisp

Packages

Some helpful ELisp packages:

  • deferred is an async API library
  • s.el is a string manipulation library
  • dash.el is a list manipulation library
  • f.el is a file manipulation library
  • request is an HTTP library
  (use-package deferred
    :commands (deferred:next
                deferred:nextc
                deferred:error
                deferred:cancel
                deferred:watch
                deferred:wait
                deferred:$
                deferred:loop
                deferred:parallel
                deferred:earlier
                deferred:call
                deferred:apply
                deferred:process
                deferred:process-buffer
                deferred:wait-idle
                deferred:url-retrieve
                deferred:url-get
                deferred:url-post
                deferred:new
                deferred:succeed
                deferred:fail
                deferred:callback
                deferred:callback-post
                deferred:errorback
                deferred:errorback-post
                deferred:try
                deferred:timeout
                deferred:process))
  (use-package s
    :defer t
    :init
    (add-hook 'emacs-startup-hook (lambda () (require 's)))
    :config
    (defun camel-case (&optional arg)
      (interactive "P")
      (let* ((bounds (bounds-of-thing-at-point 'symbol))
             (word (buffer-substring (car bounds) (cdr bounds)))
             (camel (if arg (s-upper-camel-case word)
                      (s-lower-camel-case word))))
        (delete-region (car bounds) (cdr bounds))
        (insert camel))))
  (use-package dash
    :defer t
    :init
    (add-hook 'emacs-startup-hook (lambda () (require 'dash))))
  (use-package dash-functional
    :defer t
    :init
    (add-hook 'emacs-startup-hook (lambda () (require 'dash-functional))))
  (use-package f
    :defer t
    :init
    (add-hook 'emacs-startup-hook (lambda () (require 'f))))
  (use-package request
    :commands (request request-deferred))

Editing Elisp

  (general-def '(normal motion) emacs-lisp-mode-map "C-c C-c" #'eval-defun :keymaps 'override)
  (general-def '(normal motion insert) lisp-interaction-mode-map "C-c C-c" #'eval-print-last-sexp :keymaps 'override)
  (add-hook 'ielm-mode-hook 'smartparens-strict-mode)

Load path

For machine or user specific libraries:

  (add-to-list 'load-path (expand-file-name "~/site-lisp"))

And for global ones:

  (add-to-list 'load-path "/usr/local/share/emacs/site-lisp")

Utilities

Reading a file as a string:

  (defun read-file (path)
    "Returns the contents of the file as a string"
    (with-temp-buffer
      (insert-file-contents path)
      (buffer-string)))

Opening a file as sudo:

  (defun sudo-find-file (file-name)
    "Like find file, but opens the file as root."
    (interactive "F")
    (let ((tramp-file-name (concat "/sudo::" (expand-file-name file-name))))
      (find-file tramp-file-name)))

Recursive assoc for nested alists:

  (defun assoc-recursive (alist &rest keys)
    "Recursively find KEYs in ALIST."
    (while keys
      (setq alist (cdr (assoc (pop keys) alist))))
    alist)

Format a millis timestamp into human-readable form:

  (defun format-epoch-millis (millis)
     (interactive "nTimestamp: ")
     (message (format-time-string "%F %r" (/ millis 1000))))

The same but for seconds:

  (defun format-epoch-seconds (seconds)
     (interactive "nTimestamp: ")
     (message (format-time-string "%F %r" seconds)))

Checking if a buffer contains a string:

  (defun buffer-contains-substring (string)
    (save-excursion
      (save-match-data
	(goto-char (point-min))
	(search-forward string nil t))))

Pretty-print JSON:

  (defun pprint-json (raw-json)
    (with-temp-buffer
      (insert raw-json)
      (json-pretty-print (point-min) (point-max))
      (buffer-substring (point-min) (point-max))))

Load environment variables into Emacs from a shell script:

  (cl-defun extract-vars-from-env-file (file &key dir)
    "Extracts an alist of variable name to value from
     a bash script that exports environment variables."
    (let ((file (expand-file-name file))
	  (var-re "\\(.+?\\)=\\(.+\\)$")
	  (env '()))
      (with-temp-buffer
	(cd (expand-file-name (or dir (file-name-directory file))))
	(insert (shell-command-to-string (concat "source "
						 (shell-quote-argument file)
						 " > /dev/null && env")))
	(goto-char (point-min))
	(save-match-data
	  (while (re-search-forward var-re nil t)
	    (push (cons (match-string 1) (match-string 2)) env))))
      env))

  (defun source-env-file (file)
    (interactive "fFile: \n")
    (let ((env (extract-vars-from-env-file file)))
      (dolist (binding env)
	(setenv (car binding) (cdr binding)))))

  (cl-defmacro with-env-from-file (file &rest body)
    (declare (indent 1))
    (let ((env-var (make-symbol "the-env"))
	  (path-var (make-symbol "the-path")))
      `(let* ((,env-var (extract-vars-from-env-file ,file))
	      (,path-var (assoc "PATH" ,env-var))
	      (exec-path
	       (if ,path-var
		   (append (split-string (cdr ,path-var) ":") exec-path)
		 exec-path))
	      (process-environment
	       (append
		(mapcar
		 (lambda (elt) (format "%s=%s" (car elt) (cdr elt)))
		 ,env-var)
		process-environment)))
	 ,@body)))

  (cl-defun call-with-env-from-file (file callback &key dir)
    (let* ((env (extract-vars-from-env-file file :dir dir))
	   (path (assoc "PATH" env))
	   (exec-path
	    (if path
		(append (split-string (cdr path) ":") exec-path)
	      exec-path))
	   (process-environment
	    (append (mapcar (lambda (elt) (format "%s=%s" (car elt) (cdr elt))) env)
		    process-environment)))
      (funcall callback)))

  (defmacro with-env (env &rest body)
    (declare (indent 1))
    `(let* ((process-environment
	     (append
	      (mapcar
	       (lambda (elt) (format "%s=%s" (car elt) (cdr elt)))
	       ,env)
	      process-environment)))
       ,@body))

Convenience macro to run some code in a particular default-directory:

  (defmacro with-default-directory (dir &rest body)
    (declare (indent 1))
    `(let ((default-directory ,dir))
       ,@body))
  (defun random-alnum (&optional n)
    (let* ((n-chars (or n 1))
	   (alnum "abcdefghijklmnopqrstuvwxyz0123456789")
	   (result ""))
      (dotimes (_ n-chars result)
	(let ((i (% (abs (random)) (length alnum))))
	  (setq result
		(concat result (substring alnum i (1+ i))))))))

A handy function to colorize a buffer with ANSI escape characters in it:

  (defun ansi-color (&optional begin end)
    (interactive)
    (let ((begin (or begin (point-min)))
	  (end (or end (point-max)))
	  (inhibit-read-only t))
      (ansi-color-apply-on-region begin end)))

Persisting variables between session

The idea behind this is pretty simple - variables get persisted in ~/.emacs.d/<persisted-vars-file> as a plist of (variable-name variable-value).

  (defvar persisted-vars-file "~/.emacs.d/persisted-vars")

This function retrieves the plist of persisted variables or nil if it doesn't exist:

  (defun get-persisted-plist ()
      (let ((file (expand-file-name persisted-vars-file)))
        (when (file-exists-p file)
          (let ((vars-plist-str (read-file file)))
            (unless (string= vars-plist-str "")
              (car (read-from-string vars-plist-str)))))))

This function retrieves a persisted variable:

  (defun get-persisted-var (var-name)
    "Retrieves the value of persisted variable `var-name`, or nil if not found"
    (let ((vars-plist (get-persisted-plist)))
      (when vars-plist
        (plist-get vars-plist var-name))))

And this function persists a variable:

    (defun persist-variable (var-name value)
      (let ((file (expand-file-name persisted-vars-file))
            (vars-plist (get-persisted-plist)))
        (if vars-plist
            (progn
              (plist-put vars-plist var-name value)
              (write-region
               (prin1-to-string vars-plist) nil file))
          (let ((vars-plist `(,var-name ,value)))
            (write-region
             (prin1-to-string vars-plist) nil file)))))

Process handling

Some utilities for calling out to other processes.

  (defun make-process-sentinel (success err)
    "Makes a process sentinel that calls `success` on success and `err` on error"
    (lambda (proc event)
      (cond ((string-match-p "finished" event) (funcall success))
	    (t (funcall err)))))

  (defun make-success-err-msg-sentinel (buf success-msg err-msg &optional kill-on-err)
    (make-process-sentinel
     (lambda ()
       (message success-msg)
       (kill-buffer buf))
     (lambda ()
       (message err-msg)
       (when kill-on-err
	 (kill-buffer buf)))))

A function to call a process passing some string as stdin and returning the process output:

  (defun make-process-fn (program &rest args)
    "Returns a function that, when called, call `program` with arguments `args`,
     passing the function argument as stdin"
    (lambda (&optional input)
      (with-temp-buffer
	(if input
	    (progn
	      (insert input)
	      (apply #'call-process-region (point-min) (point-max) program t t nil args))
	  (apply #'call-process program nil t nil args))
	(buffer-substring-no-properties (point-min) (point-max)))))

The same function but for commands that need to run in a shell:

  (defun make-shell-fn (program &rest args)
    "Returns a function that, when called, calls `program` in a shell
     with arguments `args`, passing the function argument as stdin"
    (lambda (&optional input)
      (let ((cmd (combine-and-quote-strings `(,program ,@args))))
	(with-temp-buffer
	 (if input
	     (progn
	       (insert input)
	       (call-shell-region (point-min) (point-max) cmd t t))
	   (call-process-shell-command cmd nil t))
	 (buffer-substring-no-properties (point-min) (point-max))))))

Running a shell command as sudo:

  (defun sudo-shell-command (command)
    (with-temp-buffer
      (cd "/sudo::/")
      (shell-command command)))

Buffer switch hooks

I want to be able to run code whenever I switch to a buffer running certain modes. The code to run is stored in a alist mapping mode names to lists of code to run (stored as a raw data structure to be eval'ed):

  (defvar buffer-mode-hooks '())

To add a new hook, push the code to run onto the correct list:

  (defun add-buffer-mode-hook (mode fn)
    (if-let ((existing-entry (assoc mode buffer-mode-hooks)))
	(push fn (cdr existing-entry))
      (let ((new-entry `(,mode . (,fn))))
	(push new-entry buffer-mode-hooks))))

Whenever the buffer changes, look up the major-mode to see if there is any code to run:

  (defun run-buffer-mode-hooks ()
    (when-let ((entry (assoc major-mode buffer-mode-hooks)))
      (dolist (fn (cdr entry))
	(funcall fn))))

  (add-hook 'buffer-list-update-hook #'run-buffer-mode-hooks)

Aliases

  (defalias 'doc 'describe-symbol)

Miscellaneous

  (setq warning-suppress-types
	'((undo discard-info)))

Dropbox

I put lots of stuff in Dropbox, but the actual folder location differs on my different computers. This function resolves to the Dropbox directory:

  (defun get-dropbox-directory ()
    (cond
     ((file-exists-p (expand-file-name "~/Dropbox (Personal)"))
      (expand-file-name "~/Dropbox (Personal)"))
     (t (expand-file-name "~/Dropbox"))))

Load up libraries from Dropbox, if there are any:

  (add-to-list 'load-path
	 (concat
	  (file-name-as-directory (get-dropbox-directory))
	  "site-lisp"))

Init File

A function to reload my init file. It reloads the major mode after the init file is loaded to rebind keymappings.

  (defun reload-init-file ()
    (interactive)
    (load-file "~/.emacs.el")
    (funcall major-mode))

And another one to edit it:

  (defun find-init-file ()
    (interactive)
    (find-file "~/init.org"))

Keybindings

These are general keybindings; those specific to certain packages are in the sections for those packages.

In some modes I want vanilla Emacs bindings:

  (with-eval-after-load 'evil
    (add-to-list 'evil-emacs-state-modes 'benchmark-init/tabulated-mode)
    (add-to-list 'evil-emacs-state-modes 'benchmark-init/tree-mode)
    (add-to-list 'evil-emacs-state-modes 'cider-stacktrace-mode)
    (add-to-list 'evil-emacs-state-modes 'geiser-debug-mode)
    (add-to-list 'evil-emacs-state-modes 'undo-tree-visualizer-mode)
    (add-to-list 'evil-emacs-state-modes 'makey-key-mode)
    (add-to-list 'evil-emacs-state-modes 'term-mode))

Visual line navigation

  (general-def 'motion "j" #'evil-next-visual-line)
  (general-def 'motion "k" #'evil-previous-visual-line)
  (general-def 'motion "j" #'evil-next-visual-line)
  (general-def 'motion "k" #'evil-previous-visual-line)

M-x

  (leader-def-key "SPC" 'execute-extended-command)

Eval-ing

  (leader-def-key ":" #'eval-expression)

Init file commands

  (jdormit/define-prefix "." "dotfile")
  (leader-def-key ".r" 'reload-init-file)
  (leader-def-key ".f" 'find-init-file)

Commands about files

  (jdormit/define-prefix "f" "files")
  (leader-def-key "ff" 'find-file)
  (leader-def-key "fd" 'dired)
  (leader-def-key "fs" 'sudo-find-file)
  (leader-def-key "ft" 'auto-revert-tail-mode)
  (leader-def-key "fp" 'find-file-at-point)
  (general-def '(normal motion visual) "gf" 'find-file-at-point)

Window commands

  (jdormit/define-prefix "w" "window")
  (leader-def-key "w/" 'split-window-right)
  (leader-def-key "w-" 'split-window-below)
  (leader-def-key "wm" 'delete-other-windows)
  (leader-def-key "wd" 'delete-window)
  (leader-def-key "ws" 'window-swap-states)

Buffer commands

A function to switch to previous buffer from this blog post:

  (defun switch-to-previous-buffer ()
    "Switch to previously open buffer.
  Repeated invocations toggle between the two most recently open buffers."
    (interactive)
    (switch-to-buffer (other-buffer (current-buffer) 1)))

  (leader-def-key "TAB" 'switch-to-previous-buffer)

A function to kill all buffers except the current one from EmacsWiki:

  (defun kill-other-buffers ()
    "Kill all other buffers."
    (interactive)
    (mapc 'kill-buffer (delq (current-buffer) (buffer-list))))
  (jdormit/define-prefix "b" "buffer")
  (leader-def-key "bb" #'switch-to-buffer)
  (leader-def-key "bn" #'next-buffer)
  (leader-def-key "bp" #'previous-buffer)
  (leader-def-key "bd" #'kill-buffer)
  (leader-def-key "bm" #'kill-other-buffers)
  (leader-def-key "br" #'rename-buffer)

Frame commands

  (jdormit/define-prefix "F" "frame")
  (leader-def-key "Fn" #'make-frame-command)
  (leader-def-key "Fo" #'other-frame)
  (leader-def-key "Fm" #'delete-other-frames)
  (leader-def-key "Fd" #'delete-frame)

Running shell commands

  (leader-def-key "!" 'shell-command)
  (leader-def-key "|" 'shell-command-on-region)

Toggles

Like in Spacemacs, put all toggle commands behind a prefix:

  (jdormit/define-prefix "t" "toggle")

Toggles about line truncation:

  (leader-def-key "tt" 'toggle-truncate-lines)
  (leader-def-key "tT" 'visual-line-mode)
  (leader-def-key "tf" 'auto-fill-mode)

Toggle lisp debugging:

  (leader-def-key "td" 'toggle-debug-on-error)

Shells/REPLs

Emacs has a shell for every mood!

  (jdormit/define-prefix "s" "shells/REPLs")
  (leader-def-key "ss" 'shell)
  (leader-def-key "si" 'ielm)
  (leader-def-key "se" 'eshell)
  (leader-def-key "sa" 'ansi-term)

Applications

  (jdormit/define-prefix "a" "applications")

Help Buffers

  (general-def 'motion help-mode-map "TAB" #'forward-button)

Code commands

  (jdormit/define-prefix "c" "code")
  (leader-def-key "cd" #'xref-find-definitions)
  (leader-def-key "cr" #'xref-find-references)
  (leader-def-key "ca" #'xref-find-apropos)
  (general-def 'normal "M-." #'xref-find-definitions)

Process list

  (general-def '(normal motion visual) process-menu-mode-map
    "d" #'process-menu-delete-process)

Evil-numbers

Handy functions to increment/decrement numbers at point:

  (use-package evil-numbers
    :straight (:host github :repo "janpath/evil-numbers")
    :general
    ((normal visual motion) "g+" 'evil-numbers/inc-at-pt)
    ((normal visual motion) "g-" 'evil-numbers/dec-at-pt)
    ((normal visual motion) "g C-+" 'evil-numbers/inc-at-pt-incremental)
    ((normal visual motion) "g C--" 'evil-numbers/dec-at-pt-incremental))

Winner

Winner is a minor mode that keeps an undo/redo history of the window configuration:

  (winner-mode 1)
  (leader-def-key "wn" #'winner-redo)
  (leader-def-key "wp" #'winner-undo)

Buffer move

A handy package to shift buffers between open windows:

  (use-package buffer-move
    :general
    ("C-S-j" #'buf-move-down)
    ("C-S-k" #'buf-move-up)
    ("C-S-h" #'buf-move-left)
    ("C-S-l" #'buf-move-right))

Info

  (use-package info
    :init
    (defhydra hydra-info (:color blue
			  :hint nil)
      "
  Info-mode:

    ^^_]_ forward  (next logical node)       ^^_l_ast (←)        _u_p (↑)                             _f_ollow reference       _T_OC
    ^^_[_ backward (prev logical node)       ^^_r_eturn (→)      _m_enu (↓) (C-u for new window)      _i_ndex                  _d_irectory
    ^^_n_ext (same level only)               ^^_H_istory         _g_oto (C-u for new window)          _,_ next index item      _c_opy node name
    ^^_p_rev (same level only)               _<_/_t_op           _b_eginning of buffer                virtual _I_ndex          _C_lone buffer
    regex _s_earch (_S_ case sensitive)      ^^_>_ final         _e_nd of buffer                      ^^                       _a_propos

    _1_ .. _9_ Pick first .. ninth item in the node's menu.

  "
      ("]"   Info-forward-node)
      ("["   Info-backward-node)
      ("n"   Info-next)
      ("p"   Info-prev)
      ("s"   Info-search)
      ("S"   Info-search-case-sensitively)

      ("l"   Info-history-back)
      ("r"   Info-history-forward)
      ("H"   Info-history)
      ("t"   Info-top-node)
      ("<"   Info-top-node)
      (">"   Info-final-node)

      ("u"   Info-up)
      ("^"   Info-up)
      ("m"   Info-menu)
      ("g"   Info-goto-node)
      ("b"   beginning-of-buffer)
      ("e"   end-of-buffer)

      ("f"   Info-follow-reference)
      ("i"   Info-index)
      (","   Info-index-next)
      ("I"   Info-virtual-index)

      ("T"   Info-toc)
      ("d"   Info-directory)
      ("c"   Info-copy-current-node-name)
      ("C"   clone-buffer)
      ("a"   info-apropos)

      ("1"   Info-nth-menu-item)
      ("2"   Info-nth-menu-item)
      ("3"   Info-nth-menu-item)
      ("4"   Info-nth-menu-item)
      ("5"   Info-nth-menu-item)
      ("6"   Info-nth-menu-item)
      ("7"   Info-nth-menu-item)
      ("8"   Info-nth-menu-item)
      ("9"   Info-nth-menu-item)

      ("?"   Info-summary "Info summary")
      ("h"   Info-help "Info help")
      ("q"   Info-exit "Info exit")
      ("C-g" nil "cancel" :color blue))
    :general
    (Info-mode-map "C-/" 'hydra-info/body))

xref

After I select an xref reference, I want the xref buffer closed:

  (defun xref-goto-xref-and-quit ()
    (interactive)
    (xref-goto-xref t))
  (general-def 'normal xref--xref-buffer-mode-map "RET"
    #'xref-goto-xref-and-quit :keymaps 'override)

Don't prompt for an identifier for these xref commands:

  (setq xref-prompt-for-identifier
	'(not xref-find-definitions
	      xref-find-definitions-other-window
	      xref-find-definitions-other-frame
	      xref-find-references))

Some keybindings:

  (general-def "M-r" #'xref-find-references)

IBuffer

  (use-package ibuffer
    :straight (:type built-in)
    :init
    (defhydra hydra-ibuffer-main (:color pink :hint nil)
      "
   ^Navigation^ | ^Mark^        | ^Actions^        | ^View^
  -^----------^-+-^----^--------+-^-------^--------+-^----^-------
    _k_:    ʌ   | _m_: mark     | _D_: delete      | _g_: refresh
   _RET_: visit | _u_: unmark   | _S_: save        | _s_: sort
    _j_:    v   | _*_: specific | _a_: all actions | _/_: filter
  -^----------^-+-^----^--------+-^-------^--------+-^----^-------
  "
      ("j" ibuffer-forward-line)
      ("RET" ibuffer-visit-buffer :color blue)
      ("k" ibuffer-backward-line)

      ("m" ibuffer-mark-forward)
      ("u" ibuffer-unmark-forward)
      ("*" hydra-ibuffer-mark/body :color blue)

      ("D" ibuffer-do-delete)
      ("S" ibuffer-do-save)
      ("a" hydra-ibuffer-action/body :color blue)

      ("g" ibuffer-update)
      ("s" hydra-ibuffer-sort/body :color blue)
      ("/" hydra-ibuffer-filter/body :color blue)

      ("o" ibuffer-visit-buffer-other-window "other window" :color blue)
      ("q" quit-window "quit ibuffer" :color blue)
      ("." nil "toggle hydra" :color blue))

    (defhydra hydra-ibuffer-mark (:color teal
				  :columns 5
				  :after-exit (hydra-ibuffer-main/body))
      "Mark"
      ("*" ibuffer-unmark-all "unmark all")
      ("M" ibuffer-mark-by-mode "mode")
      ("m" ibuffer-mark-modified-buffers "modified")
      ("u" ibuffer-mark-unsaved-buffers "unsaved")
      ("s" ibuffer-mark-special-buffers "special")
      ("r" ibuffer-mark-read-only-buffers "read-only")
      ("/" ibuffer-mark-dired-buffers "dired")
      ("e" ibuffer-mark-dissociated-buffers "dissociated")
      ("h" ibuffer-mark-help-buffers "help")
      ("z" ibuffer-mark-compressed-file-buffers "compressed")
      ("b" hydra-ibuffer-main/body "back" :color blue))

    (defhydra hydra-ibuffer-action (:color teal
				    :columns 4
				    :after-exit
				    (if (eq major-mode 'ibuffer-mode)
					(hydra-ibuffer-main/body)))
      "Action"
      ("A" ibuffer-do-view "view")
      ("E" ibuffer-do-eval "eval")
      ("F" ibuffer-do-shell-command-file "shell-command-file")
      ("I" ibuffer-do-query-replace-regexp "query-replace-regexp")
      ("H" ibuffer-do-view-other-frame "view-other-frame")
      ("N" ibuffer-do-shell-command-pipe-replace "shell-cmd-pipe-replace")
      ("M" ibuffer-do-toggle-modified "toggle-modified")
      ("O" ibuffer-do-occur "occur")
      ("P" ibuffer-do-print "print")
      ("Q" ibuffer-do-query-replace "query-replace")
      ("R" ibuffer-do-rename-uniquely "rename-uniquely")
      ("T" ibuffer-do-toggle-read-only "toggle-read-only")
      ("U" ibuffer-do-replace-regexp "replace-regexp")
      ("V" ibuffer-do-revert "revert")
      ("W" ibuffer-do-view-and-eval "view-and-eval")
      ("X" ibuffer-do-shell-command-pipe "shell-command-pipe")
      ("b" nil "back"))

    (defhydra hydra-ibuffer-sort (:color amaranth :columns 3)
      "Sort"
      ("i" ibuffer-invert-sorting "invert")
      ("a" ibuffer-do-sort-by-alphabetic "alphabetic")
      ("v" ibuffer-do-sort-by-recency "recently used")
      ("s" ibuffer-do-sort-by-size "size")
      ("f" ibuffer-do-sort-by-filename/process "filename")
      ("m" ibuffer-do-sort-by-major-mode "mode")
      ("b" hydra-ibuffer-main/body "back" :color blue))

    (defhydra hydra-ibuffer-filter (:color amaranth :columns 4)
      "Filter"
      ("m" ibuffer-filter-by-used-mode "mode")
      ("M" ibuffer-filter-by-derived-mode "derived mode")
      ("n" ibuffer-filter-by-name "name")
      ("c" ibuffer-filter-by-content "content")
      ("e" ibuffer-filter-by-predicate "predicate")
      ("f" ibuffer-filter-by-filename "filename")
      (">" ibuffer-filter-by-size-gt "size")
      ("<" ibuffer-filter-by-size-lt "size")
      ("/" ibuffer-filter-disable "disable")
      ("b" hydra-ibuffer-main/body "back" :color blue))
    :general
    ((normal motion visual insert emacs) ibuffer-mode-map "." 'hydra-ibuffer-main/body))

Speedbar

Speedbar is cool but having it open in a separate frame is annoying. This makes it open in a side window in the same frame:

  (use-package sr-speedbar
    :commands (sr-speedbar-toggle
	       sr-speedbar-open
	       sr-speedbar-select-window
	       sr-speedbar-exist-p)
    :general
    (speedbar-mode-map "q" #'sr-speedbar-close))

  (defun switch-to-speedbar ()
    (interactive)
    (unless (sr-speedbar-exist-p)
      (sr-speedbar-open))
    (sr-speedbar-select-window))

  (leader-def-key "S" #'switch-to-speedbar)

Whitespace Visualation

  (setq whitespace-line-column 80
	whitespace-style '(face lines-tail))
  (leader-def-key "tw" #'whitespace-mode)

Line Numbers

Toggle line numbers:

  (setq display-line-numbers-type 'visual)
  (leader-def-key "tn" 'display-line-numbers-mode)

Toggle line numbering mode (normal or relative):

  (defun toggle-line-number-mode ()
    (interactive)
    (when display-line-numbers
      (if (eq display-line-numbers 'visual)
	  (progn
	    (setq display-line-numbers t)
	    (setq display-line-numbers-type t))
	(progn
	  (setq display-line-numbers 'visual)
	  (setq display-line-numbers-type 'visual)))))

  (leader-def-key "tr" #'toggle-line-number-mode)

Display line numbers by default in code and org-mode buffers:

  (add-hook 'prog-mode-hook #'display-line-numbers-mode)
  (add-hook 'org-mode-hook #'display-line-numbers-mode)

Amx

A better M-x.

  (use-package amx
    :config
    (amx-mode))

Olivetti Mode

Olivetti is a minor mode for a nice writing environment.

  (use-package olivetti
    :config
    (setq-default olivetti-body-width 100)
    (setq olivetti-body-width 100)
    :commands olivetti-mode)

  (leader-def-key "to" 'olivetti-mode)

Winum

This package includes functions to switch windows by number.

  (defun winum-assign-0-to-dired-sidebar ()
    (when (equal major-mode 'dired-sidebar-mode) 10))

  (use-package winum
    :config
    (winum-mode)
    (add-to-list 'winum-assign-functions #'winum-assign-0-to-dired-sidebar)
    (leader-def-key "0" 'winum-select-window-0-or-10)
    (leader-def-key "1" 'winum-select-window-1)
    (leader-def-key "2" 'winum-select-window-2)
    (leader-def-key "3" 'winum-select-window-3)
    (leader-def-key "4" 'winum-select-window-4)
    (leader-def-key "5" 'winum-select-window-5)
    (leader-def-key "6" 'winum-select-window-6)
    (leader-def-key "7" 'winum-select-window-7)
    (leader-def-key "8" 'winum-select-window-8)
    (leader-def-key "9" 'winum-select-window-9))

I don't want which-key display "lambda" for the descriptions of these, so set a custom display function. This is stolen from Spacemacs.

  (push '(("\\(.*\\) 0" . "select-window-0") . ("\\1 0..9" . "window 0..9"))
        which-key-replacement-alist)
  (push '((nil . "select-window-[1-9]") . t) which-key-replacement-alist)

Backups and Autosaves

Store backups and autosaves in a centralized place. This should really be the default…

  (make-directory (expand-file-name "~/.emacs.d/autosaves") t)
  (setq auto-save-file-name-transforms '((".*" "~/.emacs.d/autosaves" t)))
  (setq backup-directory-alist '(("." . "~/.emacs.d/backups")))

Smartparens/Parinfer

Smartparens enables structured editing of s-expressions and other pairs:

  (use-package smartparens
    :hook ((prog-mode . smartparens-strict-mode)
           (eshell-mode . smartparens-strict-mode)
           (inferior-python-mode . smartparens-strict-mode))
    :init
    (defhydra hydra-smartparens (:hint nil)
      "
   Moving^^^^                       Slurp & Barf^^   Wrapping^^            Sexp juggling^^^^               Destructive
  ------------------------------------------------------------------------------------------------------------------------
   [_a_] beginning  [_n_] down      [_h_] bw slurp   [_R_]   rewrap        [_S_] split   [_t_] transpose   [_c_] change inner  [_w_] copy
   [_e_] end        [_N_] bw down   [_H_] bw barf    [_u_]   unwrap        [_s_] splice  [_A_] absorb      [_C_] change outer
   [_f_] forward    [_p_] up        [_l_] slurp      [_U_]   bw unwrap     [_r_] raise   [_E_] emit        [_k_] kill          [_g_] quit
   [_b_] backward   [_P_] bw up     [_L_] barf       [_(__{__[_] wrap (){}[]   [_j_] join    [_o_] convolute   [_K_] bw kill       [_q_] quit"
      ;; Moving
      ("a" sp-beginning-of-sexp)
      ("e" sp-end-of-sexp)
      ("f" sp-forward-sexp)
      ("b" sp-backward-sexp)
      ("n" sp-down-sexp)
      ("N" sp-backward-down-sexp)
      ("p" sp-up-sexp)
      ("P" sp-backward-up-sexp)

      ;; Slurping & barfing
      ("h" sp-backward-slurp-sexp)
      ("H" sp-backward-barf-sexp)
      ("l" sp-forward-slurp-sexp)
      ("L" sp-forward-barf-sexp)

      ;; Wrapping
      ("R" sp-rewrap-sexp)
      ("u" sp-unwrap-sexp)
      ("U" sp-backward-unwrap-sexp)
      ("(" sp-wrap-round)
      ("{" sp-wrap-curly)
      ("[" sp-wrap-square)

      ;; Sexp juggling
      ("S" sp-split-sexp)
      ("s" sp-splice-sexp)
      ("r" sp-raise-sexp)
      ("j" sp-join-sexp)
      ("t" sp-transpose-sexp)
      ("A" sp-absorb-sexp)
      ("E" sp-emit-sexp)
      ("o" sp-convolute-sexp)

      ;; Destructive editing
      ("c" sp-change-inner :exit t)
      ("C" sp-change-enclosing :exit t)
      ("k" sp-kill-sexp)
      ("K" sp-backward-kill-sexp)
      ("w" sp-copy-sexp)

      ("q" nil)
      ("g" nil))
    :config
    (require 'smartparens-config)
    (show-smartparens-global-mode t)
    :general
    (prog-mode-map "C-c p" 'hydra-smartparens/body)
    (normal prog-mode-map "g p" 'hydra-smartparens/body)
    (normal "g[" 'sp-wrap-square)
    (normal "g(" 'sp-wrap-round)
    (normal "g{" 'sp-wrap-curly))

  (use-package evil-smartparens
    :after (evil smartparens)
    :hook ((smartparens-enabled . evil-smartparens-mode)))

  (jdormit/define-prefix "l" "lisp")
  (jdormit/define-prefix "lw" "wrap")
  (leader-def-key "lwr" 'sp-wrap-round)
  (leader-def-key "lws" 'sp-wrap-square)
  (leader-def-key "lwc" 'sp-wrap-curly)
  (leader-def-key "ls" 'sp-forward-slurp-sexp)
  (leader-def-key "lb" 'sp-forward-barf-sexp)

Enable ES6 arrow functions in web-mode ("borrowed" from this GitHub comment):

  (with-eval-after-load 'smartparens
    (defun sp-after-equals-p (_id action _context)
      (when (memq action '(insert navigate))
	(sp--looking-back-p "=>" 2)))

    (defun sp-after-equals-skip (ms mb _me)
      (when (eq ms ">")
	(save-excursion
	  (goto-char mb)
	  (sp--looking-back-p "=" 1))))

    (sp-local-pair '(web-mode) "<" nil
		   :unless '(:add sp-after-equals-p)
		   :skip-match 'sp-after-equals-skip-p))

Parinfer infers parens from indentation and vice-versa. Currently disabled since it turned out to be more annoying than good…

  (use-package parinfer
    :disabled
    :init
    (leader-def-key "lt" 'parinfer-toggle-mode)
    (setq parinfer-extensions '(defaults
				 pretty-parens
				 evil
				 smart-tab
				 smart-yank))
    :hook ((clojure-mode . parinfer-mode)
	   (emacs-lisp-mode . parinfer-mode)
	   (common-lisp-mode . parinfer-mode)
	   (scheme-mode . parinfer-mode)
	   (lisp-mode . parinfer-mode)))

jq

The JSON multitool.

  (use-package jq-mode
    :commands (jq-mode jq-interactively))

Org Mode

Notes, agenda, calendar, blogging, journaling, etc.

  (jdormit/define-prefix "o" "org")
  (leader-def-key "oa" 'org-agenda)
  (leader-def-key "oc" 'org-capture)
  (setq org-src-fontify-natively t
        org-ellipsis " ▼"
        org-directory (concat (get-dropbox-directory) "/org")
        org-link-elisp-confirm-function 'y-or-n-p
        org-startup-with-inline-images t)
  (add-hook 'org-mode-hook #'auto-fill-mode)

Use RET to follow links:

  (setq org-return-follows-link t)

Always show inline images:

  (add-hook 'org-mode-hook
    (lambda ()
      (org-display-inline-images nil t)
      (org-redisplay-inline-images)))

Include a timestamp when TODO entries are closed:

  (setq org-log-done 'time)

Tell Emacs how to open file links of various types:

  (with-eval-after-load 'org
    (add-to-list 'org-file-apps '(directory . emacs))
    (add-to-list 'org-file-apps '("log" . emacs)))

A function to add ids to every heading in an Org file:

  (defun org-get-create-ids-all ()
    (interactive)
    (save-excursion
      (goto-char (point-max))
      (while (outline-previous-heading)
	(org-id-get-create))))

Agenda files

  (defun agenda-files (&optional file)
    (let ((agenda-dir (concat (file-name-as-directory (get-dropbox-directory)) "org")))
      (if file
	 (concat (file-name-as-directory agenda-dir) file)
	 agenda-dir)))

  (setq org-agenda-files `(,(agenda-files)))

Capture templates

  (setq org-capture-templates
	`(("L" "Lola task" entry
	   (file+headline ,(agenda-files "todo.org") "Lola")
	   "* TODO %i%?")
	  ("p" "Personal task" entry
	   (file+headline ,(agenda-files "todo.org") "Personal")
	   "* TODO %i%?")
	  ("n" "Note" entry
	   (file ,(agenda-files "notes.org"))
	   "* %^{Description}\n%i%?")
	  ("l" "Log" entry
	   (file ,(agenda-files "log.org"))
	   "* %<%Y-%m-%d %H:%M:%S>%?")))

Refile targets

  (setq org-refile-use-outline-path 'file
	org-refile-targets `((org-agenda-files :level . 0)
			     (,(agenda-files "notes.org") :level . 1)
			     (,(agenda-files "todo.org") :level . 1)))

Todo keywords

  (setq org-todo-keywords
        '((sequence "TODO(t)" "IN PROGRESS(i)" "BLOCKED(b)" "|" "DONE(d)" "CANCELLED(c)")))

Agenda views

  (setq org-agenda-todo-ignore-scheduled 'future
	org-agenda-tags-todo-honor-ignore-options t
	org-agenda-span 'day
	org-agenda-custom-commands
	'(("L" "Lola" ((tags-todo "@lola")))
	  ("t" "TODOs"
	   ((agenda)
	    (alltodo)))))

Keybindings

  (general-def 'normal org-mode-map "T" #'org-insert-todo-heading)
  (general-def 'normal org-mode-map "K" #'org-move-subtree-up)
  (general-def 'normal org-mode-map "J" #'org-move-subtree-down)
  (general-def 'normal org-mode-map "<return>" #'org-return)
  (general-def 'normal org-mode-map "TAB" #'org-cycle)
  (general-def 'normal org-mode-map "SPC" leader-map)
  (general-def '(normal motion visual) org-mode-map "gn" #'org-next-link)
  (general-def '(normal motion visual) org-mode-map "gp" #'org-previous-link)
  (general-def org-mode-map "C-c e" #'org-preview-latex-fragment)
  (general-def "C-c l" #'org-store-link)

Set up evil keybindings:

  (use-package evil-org
    :after (evil org)
    :hook (org-mode . evil-org-mode)
    :config
    (add-hook 'evil-org-mode-hook
              (lambda ()
                (evil-org-set-key-theme
                 '(textobjects
                   insert
                   navigation
                   additional
                   shift
                   todo))
                (general-def 'insert org-mode-map [backspace] 'org-delete-backward-char)))
    (require 'evil-org-agenda)
    (evil-org-agenda-set-keys)
    (general-def 'motion org-agenda-mode-map "SPC" leader-map)
    (general-def 'motion org-agenda-mode-map "gs" 'org-save-all-org-buffers)
    (general-def '(normal motion) evil-org-mode-map
      "C-S-j" nil
      "C-S-k" nil
      "C-S-h" nil
      "C-S-l" nil))

And a global keybinding to open an org file:

  (defun find-org-file (file)
    (interactive
     (list (completing-read
	    "Find org file: "
	    (directory-files (agenda-files) t))))
    (find-file file))

  (leader-def-key "fo" #'find-org-file)

Finally, add a helpful agenda-mode hydra:

  (with-eval-after-load 'org-agenda
    (defhydra hydra-org-agenda (:pre (setq which-key-inhibit t)
				:post (setq which-key-inhibit nil)
				:hint none)
      "
  Org agenda (_q_uit)

  ^Clock^      ^Visit entry^              ^Date^             ^Other^
  ^-----^----  ^-----------^------------  ^----^-----------  ^-----^---------
  _ci_ in      _SPC_ in other window      _ds_ schedule      _gr_ reload
  _co_ out     _TAB_ & go to location     _dd_ set deadline  _._  go to today
  _cq_ cancel  _RET_ & del other windows  _dt_ timestamp     _gd_ go to date
  _cj_ jump    _o_   link                 _+_  do later      ^^
  ^^           ^^                         _-_  do earlier    ^^
  ^^           ^^                         ^^                 ^^
  ^View^          ^Filter^                 ^Headline^         ^Toggle mode^
  ^----^--------  ^------^---------------  ^--------^-------  ^-----------^----
  _vd_ day        _ft_ by tag              _ht_ set status    _tf_ follow
  _vw_ week       _fr_ refine by tag       _hk_ kill          _tl_ log
  _vt_ fortnight  _fc_ by category         _hr_ refile        _ta_ archive trees
  _vm_ month      _fh_ by top headline     _hA_ archive       _tA_ archive files
  _vy_ year       _fx_ by regexp           _h:_ set tags      _tr_ clock report
  _vn_ next span  _fd_ delete all filters  _hp_ set priority  _td_ diaries
  _vp_ prev span  ^^                       ^^                 ^^
  _vr_ reset      ^^                       ^^                 ^^
  ^^              ^^                       ^^                 ^^
  "
      ;; Entry
      ("hA" org-agenda-archive-default)
      ("hk" org-agenda-kill)
      ("hp" org-agenda-priority)
      ("hr" org-agenda-refile)
      ("h:" org-agenda-set-tags)
      ("ht" org-agenda-todo)
      ;; Visit entry
      ("o"   link-hint-open-link :exit t)
      ("<tab>" org-agenda-goto :exit t)
      ("TAB" org-agenda-goto :exit t)
      ("SPC" org-agenda-show-and-scroll-up)
      ("RET" org-agenda-switch-to :exit t)
      ;; Date
      ("dt" org-agenda-date-prompt)
      ("dd" org-agenda-deadline)
      ("+" org-agenda-do-date-later)
      ("-" org-agenda-do-date-earlier)
      ("ds" org-agenda-schedule)
      ;; View
      ("vd" org-agenda-day-view)
      ("vw" org-agenda-week-view)
      ("vt" org-agenda-fortnight-view)
      ("vm" org-agenda-month-view)
      ("vy" org-agenda-year-view)
      ("vn" org-agenda-later)
      ("vp" org-agenda-earlier)
      ("vr" org-agenda-reset-view)
      ;; Toggle mode
      ("ta" org-agenda-archives-mode)
      ("tA" (org-agenda-archives-mode 'files))
      ("tr" org-agenda-clockreport-mode)
      ("tf" org-agenda-follow-mode)
      ("tl" org-agenda-log-mode)
      ("td" org-agenda-toggle-diary)
      ;; Filter
      ("fc" org-agenda-filter-by-category)
      ("fx" org-agenda-filter-by-regexp)
      ("ft" org-agenda-filter-by-tag)
      ("fr" org-agenda-filter-by-tag-refine)
      ("fh" org-agenda-filter-by-top-headline)
      ("fd" org-agenda-filter-remove-all)
      ;; Clock
      ("cq" org-agenda-clock-cancel)
      ("cj" org-agenda-clock-goto :exit t)
      ("ci" org-agenda-clock-in :exit t)
      ("co" org-agenda-clock-out)
      ;; Other
      ("q" nil :exit t)
      ("gd" org-agenda-goto-date)
      ("." org-agenda-goto-today)
      ("gr" org-agenda-redo))
    (general-def '(normal visual motion insert emacs) org-agenda-mode-map "." 'hydra-org-agenda/body))

Exporting

HTML

Export to HTML:

  (use-package htmlize
    :commands htmlize-buffer)

Don't put section numbers in front of headers:

  (setq org-export-with-section-numbers nil)

Disable the preamble and postamble:

  (setq org-html-preamble nil
	org-html-postamble nil)

Redefine org-html-src-block to wrap code blocks in <pre><code> and language class for use by highlight.js:

  (defun org-html-src-block (src-block _contents info)
    "Transcode a SRC-BLOCK element from Org to HTML.
  CONTENTS holds the contents of the item.  INFO is a plist holding
  contextual information."
    (if (org-export-read-attribute :attr_html src-block :textarea)
	(org-html--textarea-block src-block)
      (let* ((lang (org-element-property :language src-block))
	     (code (org-html-format-code src-block info))
	     (label (let ((lbl (and (org-element-property :name src-block)
				    (org-export-get-reference src-block info))))
		      (if lbl (format " id=\"%s\"" lbl) "")))
	     (klipsify  (and  (plist-get info :html-klipsify-src)
			      (member lang '("javascript" "js"
					     "ruby" "scheme" "clojure" "php" "html")))))
	(if (not lang) (format "<pre><code class=\"example\"%s>\n%s</code></pre>" label code)
	  (format "<div class=\"org-src-container\">\n%s%s\n</div>"
		  ;; Build caption.
		  (let ((caption (org-export-get-caption src-block)))
		    (if (not caption) ""
		      (let ((listing-number
			     (format
			      "<span class=\"listing-number\">%s </span>"
			      (format
			       (org-html--translate "Listing %d:" info)
			       (org-export-get-ordinal
				src-block info nil #'org-html--has-caption-p)))))
			(format "<label class=\"org-src-name\">%s%s</label>"
				listing-number
				(org-trim (org-export-data caption info))))))
		  ;; Contents.
		  (if klipsify
		      (format "<pre><code class=\"src src-%s\"%s%s>%s</code></pre>"
			      lang
			      label
			      (if (string= lang "html")
				  " data-editor-type=\"html\""
				"")
			      code)
		    (format "<pre><code class=\"src %s src-%s\"%s>%s</code></pre>"
			    lang lang label code)))))))

Github-flavored markdown

  (use-package ox-gfm
    :defer t
    :init
    (with-eval-after-load 'org
      (require 'ox-gfm)))

Jira

  (use-package ox-jira
    :defer t
    :straight (:host github :repo "stig/ox-jira.el")
    :init
    (with-eval-after-load 'org
      (require 'ox-jira)))

org-babel

Literate programming!

  (add-hook 'org-mode-hook
    (lambda ()
      (org-babel-do-load-languages
       'org-babel-load-languages
       '((emacs-lisp . t)
	 (python . t)
	 (shell . t)
	 (clojure . t)
	 (lisp . t)
	 (scheme . t)
	 (java . t)
	 (js . t)
	 (dot . t)
	 (ditaa . t)
	 (ledger . t)
	 (sql . t)
	 (jq . t)
         (restclient . t)))))

Get rid of the confirmation prompt:

  (setq org-confirm-babel-evaluate nil)

Display inline images after executing a source block:

  (add-hook 'org-babel-after-execute-hook
      (lambda ()
	(org-display-inline-images nil t)
	(org-redisplay-inline-images)))

Enable async source block execution. Note: this execute the contents of the source block in a separate Emacs process, so blocks that rely on anything defined in init.org or the current Emacs process won't work as expected.

  (use-package ob-async
    :straight (ob-async :host github :repo "astahlman/ob-async")
    :defer t
    :hook (org-mode . (lambda () (require 'ob-async))))

Filter out the "u" from unicode results in org tabels returned from Python source blocks:

  (with-eval-after-load 'ob-python
    (defun org-babel-python-table-or-string (results)
      "Convert RESULTS into an appropriate elisp value.
  If the results look like a list or tuple, then convert them into an
  Emacs-lisp table, otherwise return the results as a string."
      (let ((res (org-babel-script-escape results)))
	(if (listp res)
	    (mapcar (lambda (el)
		      (cond
		       ((eq el 'None) org-babel-python-None-to)
		       ((listp el) (-filter (lambda (m) (not (eq m 'u))) el))
		       (t el)))
		    res)
	  res))))

Images

  (setq org-image-actual-width nil)

org-scratch

It's very useful to open a new org buffer for a quick org-babel exploration.

  (defun org-scratch (&optional make-new)
    "Switches to an org-mode buffer not associated with any file.
  If called with a prefix argument, always creates a new buffer;
  otherwise, switches to the existing *org-scratch* buffer if it exists."
    (interactive "P")
    (let ((org-scratch-buffer-name
	   (generate-new-buffer-name
	    "*org-scratch*"
	    (unless make-new "*org-scratch*"))))
      (switch-to-buffer org-scratch-buffer-name)
      (org-mode)))

  (leader-def-key "os" #'org-scratch)

org-gcal

Integrate Google calendar with org-mode:

  (defun get-calendar-file (name)
    (concat (file-name-as-directory (get-dropbox-directory))
	    "org/" name))

  (use-package org-gcal
    :after (org)
    :commands (org-gcal-sync
	       org-gcal-fetch
	       org-gcal-post-at-point
	       org-gcal-delete-at-point
	       org-gcal-refresh-token)
    :config
    (setq org-gcal-client-id (password-store-get "lola-org-gcal-client-id")
	  org-gcal-client-secret (password-store-get "lola-org-gcal-client-secret")
	  org-gcal-file-alist `(("jeremydormitzer@lola.com" . ,(get-calendar-file "lola-gcal.org"))
				("jeremy.dormitzer@gmail.com" . ,(get-calendar-file "personal-gcal.org"))
				("lut2o2moohg6qkdsto1qfq7th4@group.calendar.google.com" . ,(get-calendar-file "j-n-gcal.org")))
	  org-gcal-notify-p nil))

  (defun org-gcal-fetch-and-save ()
    (interactive)
    (deferred:$
      (org-gcal-fetch)
      (deferred:nextc it
	(lambda ()
	  (dolist (entry org-gcal-file-alist)
	    (with-current-buffer (cdr entry)
	      (save-buffer)))))))

  (add-hook 'emacs-startup-hook #'org-gcal-fetch-and-save)

  (defun org-agenda-redo-and-fetch-gcal (&optional all)
    (interactive "P")
    (let ((cb (if all #'org-agenda-redo-all #'org-agenda-redo)))
      (deferred:nextc (org-gcal-fetch-and-save) cb)))

  (with-eval-after-load 'org-agenda
   (general-def '(normal motion) org-agenda-mode-map "gR" #'org-agenda-redo-and-fetch-gcal))

Utilities

A function to get the TITLE property of the current org buffer:

  (defun org-get-title (&optional contents)
    (let ((raw (or contents
		   (buffer-substring (point-min) (point-max)))))
      (with-temp-buffer
	(insert raw)
	(car
	 (org-element-map
	     (org-element-parse-buffer)
	     'keyword
	   (lambda (el)
	     (when (string-match-p "TITLE"
				   (org-element-property :key el))
	       (org-element-property :value el))))))))

org-present

An ultra-minimalist presentation mode for Org:

  (use-package org-present
    :commands (org-present)
    :config
    (defun org-present-on ()
      (org-present-big)
      (org-display-inline-images)
      (org-present-hide-cursor)
      (display-line-numbers-mode -1)
      (org-present-read-only))
    (defun org-present-off ()
      (org-present-small)
      (org-present-show-cursor)
      (unless org-startup-with-inline-images
       (org-remove-inline-images))
      (display-line-numbers-mode)
      (org-present-read-write))
    (add-hook 'org-present-mode-hook #'org-present-on)
    (add-hook 'org-present-mode-quit-hook #'org-present-off)
    ;; Redefine org-present to call org-present-narrow after running hooks
    (defun org-present ()
      "init."
      (interactive)
      (setq org-present-mode t)
      (org-present-add-overlays)
      (run-hooks 'org-present-mode-hook)
      (org-present-narrow)
      (org-present-run-after-navigate-functions))
    :init
    (general-def '(normal visual motion emacs) org-present-mode-keymap
     "<left>" #'org-present-prev
     "<right>" #'org-present-next
     "C-k" #'org-present-prev
     "C-j" #'org-present-next
     "q" #'org-present-quit))

link-hint

A very helpful package that provides jump-to-link functionality:

  (use-package link-hint
    :commands (link-hint-open-link
	       link-hint-copy-link)
    :init
    (jdormit/define-prefix "ol" "link-hint")
    (leader-def-key "oll" #'link-hint-open-link)
    (leader-def-key "olc" #'link-hint-copy-link))

org-cliplink

Intelligently inserts an org-mode link from the clipboard.

  (use-package org-cliplink
    :commands (org-cliplink
	       org-cliplink-clipboard-content)
    :general
    (org-mode-map "C-c C-S-l" #'org-cliplink))

org-board

Org-board is a bookmarking/archiving system built on Org mode:

  (use-package org-board
    :defer t
    :after org
    :init

    ;; Org-capture setup

    (defvar org-capture-bookmark-last-url nil)
    (defvar org-capture-bookmark-last-title nil)

    (defun org-capture-bookmark-get-url ()
      (let* ((clip (org-cliplink-clipboard-content))
	     (parsed (url-generic-parse-url clip)))
	(if (url-type parsed)
	    clip
	  (read-string "Bookmark URL: "))))

    (defun org-capture-bookmark-get-title (url)
      (or (org-cliplink-retrieve-title-synchronously url)
	  (read-string "Bookmark title: ")))

    (defun bookmark-file (title)
      (concat (get-dropbox-directory)
	      (format "/org/%s.org" (org-roam--get-new-id title))))

    (defun org-capture-bookmark-file ()
      (let* ((url (org-capture-bookmark-get-url))
	     (title (org-capture-bookmark-get-title url)))
	(setq org-capture-bookmark-last-url url)
	(setq org-capture-bookmark-last-title title)
	(bookmark-file title)))

    (defun org-capture-bookmark-link ()
      (format "[[%s][%s]]"
	      org-capture-bookmark-last-url
	      org-capture-bookmark-last-title))

    (defun org-capture-bookmark-title ()
      org-capture-bookmark-last-title)

    (defun save-bookmark (url)
      (save-excursion
	(goto-char (point-min))
	(when (search-forward "* Bookmark" nil t)
	  (org-board-new url)
	  (wallabag-add-entry url
			      (cl-function
			       (lambda (&key data &allow-other-keys)
				 (let ((entry-url (format "%s/view/%s"
							  wallabag-base-url
							  (alist-get 'id data))))
				   (message "Added bookmark to Wallabag: %s" entry-url))))))))

    (defun org-capture-bookmark-after-finalize ()
      "Runs `org-board-new' on the captured entry.
    Also saves to Wallabag."
      (let ((success (not org-note-abort))
	    (key (plist-get org-capture-plist :key))
	    (desc (plist-get org-capture-plist :description)))
	(when (and success
		   (equal key "b")
		   (equal desc "Bookmark")
		   org-capture-bookmark-last-url)
	  (save-bookmark org-capture-bookmark-last-url)
	  (setq org-capture-bookmark-last-url nil)
	  (setq org-capture-bookmark-last-title nil))))

    (add-hook 'org-capture-prepare-finalize-hook
	      #'org-capture-bookmark-after-finalize)

    (add-to-list 'org-capture-templates
		 '("b" "Bookmark" plain
		   (file org-capture-bookmark-file)
		   "#+TITLE: %(org-capture-bookmark-title)\n\n- tags :: [[file:deft/bookmarks.org][Bookmarks]]\n- source :: %(org-capture-bookmark-link)\n%?\n* Bookmark"))

    ;; Org-protocol setup
    (defun make-org-protocol-bookmark (url title)
      (with-temp-buffer
	(let ((filename (bookmark-file title)))
	  (save-excursion
	    (insert (concat (format "#+TITLE: %s\n\n" title)
			    "- tags :: [[file:deft/bookmarks.org][Bookmarks]]\n"
			    (format "- source :: [[%s][%s]]\n\n" url title)
			    "* Bookmark"))
	    (write-file filename)
	    (save-bookmark url)
	    (save-buffer)))))

    (defun bookmark-via-org-protocol (url)
      (org-cliplink-retrieve-title (url-unhex-string url) #'make-org-protocol-bookmark))

    (add-to-list 'org-protocol-protocol-alist
		 '("Bookmark"
		   :protocol "bookmark"
		   :function bookmark-via-org-protocol
		   :kill-client t))

    :config
    (add-to-list 'org-board-wget-switches "--recursive")
    (add-to-list 'org-board-wget-switches "--level=1")
    (add-to-list 'org-board-wget-switches "--span-hosts")
    ;; Use w3m instead of eww to open org-board archived links
    (advice-add 'org-board-open-with :around
		(lambda (oldfn filename-string arg &rest args)
		  (cond
		   ((not (file-exists-p filename-string)) 1)
		   ((and filename-string
			 (or (and arg (eq org-board-default-browser 'system))
			     (and (not arg) (eq org-board-default-browser 'eww))))
		    (let ((filename (concat "file://"
					    (s-chop-prefix "file://"
							   filename-string))))
		      (w3m filename t)
		      0))
		   (:else (apply oldfn filename-string arg args)))))
    :general
    (org-mode-map "C-c b" org-board-keymap))

org-rifle

Quickly find stuff in Org files:

  (use-package helm-org-rifle
    :defer t
    :init
    (defvar helm-org-rifle-commands-map (make-sparse-keymap))
    (general-def helm-org-rifle-commands-map
      "r" #'helm-org-rifle
      "a" #'helm-org-rifle-agenda-files
      "b" #'helm-org-rifle-current-buffer
      "d" #'helm-org-rifle-directories
      "f" #'helm-org-rifle-files
      "o" #'helm-org-rifle-org-directory
      "R" #'helm-org-rifle-occur
      "A" #'helm-org-rifle-occur-agenda-files
      "B" #'helm-org-rifle-occur-current-buffer
      "D" #'helm-org-rifle-occur-directories
      "F" #'helm-org-rifle-occur-files
      "O" #'helm-org-rifle-occur-org-directory)
    (leader-def-key "or" helm-org-rifle-commands-map)
    (jdormit/define-prefix "or" "org-rifle"))

Org Noter

Org Noter lets me take org-mode notes on PDFs, epubs, and DocView files:

  (use-package org-noter
    :commands org-noter
    :general
    ((normal visual motion) org-noter-doc-mode-map "i" #'org-noter-insert-note)
    ((normal visual motion) org-noter-doc-mode-map "q" #'org-noter-kill-session)
    ((normal visual motion) org-noter-doc-mode-map "C-M-n" #'org-noter-sync-next-note)
    ((normal visual motion) org-noter-doc-mode-map "C-M-p" #'org-noter-sync-prev-note)
    ((normal visual motion) org-noter-doc-mode-map "M-." #'org-noter-sync-current-page-or-chapter)
    ((normal visual motion) org-noter-doc-mode-map "M-i" #'org-noter-insert-precise-note)
    ((normal visual motion) org-noter-doc-mode-map "M-n" #'org-noter-sync-next-page-or-chapter)
    ((normal visual motion) org-noter-doc-mode-map "M-p" #'org-noter-sync-prev-page-or-chapter)
    ((normal visual motion) org-noter-doc-mode-map "C-M-." #'org-noter-sync-current-note))

Org Roam

Org-roam is another backlink package for org-mode:

  (use-package org-roam
    :after org
    :straight (:host github :repo "jethrokuan/org-roam")
    :commands
    (org-roam
     org-roam-today
     org-roam-find-file
     org-roam-insert
     org-roam-show-graph
     org-roam--get-new-id)
    :hook
    (after-init . org-roam-mode)
    :custom
    (org-roam-directory (concat (get-dropbox-directory) "/org"))
    :init
    (leader-def-key "fo" #'org-roam-find-file)
    (leader-def-key "of" #'org-roam-find-file)
    (defvar org-roam-map (make-sparse-keymap))
    (leader-def-key "on" org-roam-map)
    (jdormit/define-prefix "on" "org-roam")
    (which-key-add-key-based-replacements "C-c n" "org-roam")
    (which-key-add-major-mode-key-based-replacements
      'org-mode "gn" "org-roam")
    :config
    (add-hook 'org-roam-backlinks-mode-hook #'olivetti-mode)
    :general
    (org-roam-map "l" #'org-roam)
    (org-roam-map "t" #'org-roam-today)
    (org-roam-map "f" #'org-roam-find-file)
    (org-roam-map "i" #'org-roam-insert)
    (org-roam-map "g" #'org-roam-show-graph)
    ((normal motion visual) org-mode-map "gr" org-roam-map)
    ((normal motion visual) org-roam-backlinks-mode-map "<return>" #'org-open-at-point)
    ((normal motion visual emacs) org-roam-backlinks-mode-map "q" #'quit-window)
    ("C-c n" org-roam-map))

org-journal

org-journal is a package that provides some convenience functions around keeping a daily Org-mode journal. I also set it up so it plays nice with Org-roam:

  (use-package org-journal
    :defer t
    :init
    (defun org-journal-file-header-func ()
      (let ()
	(format "#+TITLE: %s"
		(format-time-string "%Y-%m-%d"))))
    (defun org-journal-today ()
      (interactive)
      (org-journal-new-entry t))
    (defun org-journal-capture-func ()
      (org-journal-new-entry t)
      (goto-char (point-min))
      ;; Account for the #+TITLE
      (forward-line))
    (add-to-list 'org-capture-templates
		 '("j" "Journal entry" entry (function org-journal-capture-func)
		   "* %(format-time-string org-journal-time-format)\n%?"))
    (jdormit/define-prefix "oj" "org-journal")
    (leader-def-key "ojn" #'org-journal-new-entry)
    (leader-def-key "ojt" #'org-journal-today)
    :custom
    (org-journal-file-type 'daily)
    (org-journal-dir (concat (get-dropbox-directory) "/org"))
    (org-journal-file-format "%Y-%m-%d.org")
    (org-journal-file-header 'org-journal-file-header-func)
    (org-journal-carryover-items "")
    :general
    (org-roam-map "t" 'org-journal-today))

org-super-agenda

  (use-package org-super-agenda
    :init
    (setq org-super-agenda-groups
          '((:name "In progress"
                   :todo "IN PROGRESS")
            (:name "Lola"
                   :tag "@lola")
            (:name "unifyDB"
                   :tag "@unifydb")
            (:name "Personal"
                   :tag "@personal")))
    (org-super-agenda-mode)
    :config
    (setq org-super-agenda-header-map (make-sparse-keymap)))

Projectile

  (use-package projectile
    :commands (projectile-find-file
	       projectile-grep
	       projectile-switch-project
	       projectile-project-root)
    :init
    (defhydra hydra-projectile (:color teal :hint nil) "
    PROJECTILE: %(projectile-project-root)

       Find File Search/Tags Buffers Cache
  ------------------------------------------------------------------------------------------
  _s-f_: file _a_: ag _i_: Ibuffer _c_: cache clear _ff_: file dwim
   _g_: update gtags _b_: switch to buffer _x_: remove known project
   _fd_: file curr dir _o_: multi-occur _s-k_: Kill all buffers _X_:
   cleanup non-existing _r_: recent file ^^^^_z_: cache current _d_:
   dir

  "
      ("a" projectile-ag)
      ("b" projectile-switch-to-buffer)
      ("c" projectile-invalidate-cache)
      ("d" projectile-find-dir)
      ("s-f" projectile-find-file)
      ("ff" projectile-find-file-dwim)
      ("fd" projectile-find-file-in-directory)
      ("g" ggtags-update-tags)
      ("s-g" ggtags-update-tags)
      ("i" projectile-ibuffer)
      ("K" projectile-kill-buffers)
      ("s-k" projectile-kill-buffers)
      ("m" projectile-multi-occur)
      ("o" projectile-multi-occur)
      ("s-p" projectile-switch-project "switch project")
      ("p" projectile-switch-project)
      ("s" projectile-switch-project)
      ("r" projectile-recentf)
      ("x" projectile-remove-known-project)
      ("X" projectile-cleanup-known-projects)
      ("z" projectile-cache-current-file)
      ("`" hydra-projectile-other-window/body "other window")
      ("q" nil "cancel" :color blue))
    :config
    (projectile-mode)
    (jdormit/define-prefix "p" "projectile")
    (leader-def-key "p" projectile-command-map))

  (defmacro with-projectile-root (&rest body)
    `(with-temp-buffer
       (when (projectile-project-root)
	 (cd (projectile-project-root)))
       ,@body))

Mode line

UI

Get rid of the janky buttons:

  (tool-bar-mode -1)

And the menu bar:

  (menu-bar-mode -1)

And the ugly scroll bars:

  (set-scroll-bar-mode nil)

Use variable-pitch-mode in text modes:

  (add-hook 'text-mode-hook (lambda () (variable-pitch-mode)))
  (add-hook 'w3m-mode-hook (lambda () (variable-pitch-mode)))

Always use buffer-face-mode in code and text buffers:

  (add-hook 'prog-mode-hook #'buffer-face-mode)
  (add-hook 'text-mode-hook #'buffer-face-mode)

Display the column number in programming modes:

  (add-hook 'prog-mode-hook #'column-number-mode)

Disable the bell:

  (setq ring-bell-function 'ignore)

Render stuff differently based on whether or not we are graphical:

  (defun graphical-setup ()
    (when (display-graphic-p (selected-frame))
      (message "Running graphically")))

  (defun non-graphical-setup ()
    (when (not (display-graphic-p (selected-frame)))
      (message "Running in terminal")
      (menu-bar-mode -1)))

  (defun do-graphical-non-graphical-setup ()
    (graphical-setup)
    (non-graphical-setup))

  (add-hook 'window-setup-hook #'do-graphical-non-graphical-setup)

Try to make the background normal colored in the terminal:

  (defvar no-background-in-tty-faces '(default line-number magit-section-highlight))

  (defun on-frame-open (frame)
    (unless (display-graphic-p frame)
      (menu-bar-mode -1)
      (dolist (face no-background-in-tty-faces)
       (set-face-background face "unspecified" frame))))

  (mapc #'on-frame-open (frame-list))

  (add-hook 'after-make-frame-functions #'on-frame-open)

  (defun on-after-init ()
    (unless (display-graphic-p (selected-frame))
      (dolist (face no-background-in-tty-faces)
	(set-face-background face "unspecified" (selected-frame)))))

  (add-hook 'window-setup-hook #'on-after-init)

UI-related keybindings:

  (jdormit/define-prefix "u" "UI")
  (leader-def-key "ut" #'customize-themes)
  (leader-def-key "uf" #'customize-face)
  (leader-def-key "uc" #'display-time-mode)
  (leader-def-key "ub" #'display-battery-mode)

Centaur tabs

Centaur tabs is a package that gives Emacs buffer tabs similar to those in Atom or VS Code:

  (use-package centaur-tabs
    :commands (centaur-tabs-mode
               centaur-tabs-local-mode
               centaur-tabs-mode-on-p)
    :init
    (setq centaur-tabs-set-icons t
          centaur-tabs-gray-out-icons 'buffer
          centaur-tabs-height 30
          centaur-tabs-set-bar 'under
          x-underline-at-descent-line t
          centaur-tabs-set-modified-marker t
          centaur-tabs-show-navigation-buttons t
          centaur-tabs-down-tab-text " ☰ "
          centaur-tabs-backward-tab-text " ◀ "
          centaur-tabs-forward-tab-text " ▶ "
          centaur-tabs-close-button "✕"
          centaur-tabs-modified-marker "⬤"
          centaur-tabs-cycle-scope 'tabs
          centaur-tabs-label-fixed-length 20)
    (leader-def-key "uT" #'centaur-tabs-mode)
    (centaur-tabs-mode)
    :config
    (centaur-tabs-group-by-projectile-project)

    ;; Custom buffer groups
    (defun centaur-tabs-projectile-buffer-groups ()
    "Return the list of group names BUFFER belongs to."
    (if centaur-tabs-projectile-buffer-group-calc
        (symbol-value 'centaur-tabs-projectile-buffer-group-calc)
      (set (make-local-variable 'centaur-tabs-projectile-buffer-group-calc)

           (cond
            ((or (get-buffer-process (current-buffer))
                 (memq major-mode '(comint-mode compilation-mode))) '("Term"))
            ((string-equal "*" (substring (buffer-name) 0 1)) '("Misc"))
            ((condition-case _err
                 (projectile-project-root)
               (error nil)) (list (projectile-project-name)))
            ((memq major-mode '(emacs-lisp-mode python-mode emacs-lisp-mode c-mode
                                                c++-mode javascript-mode js-mode
                                                js2-mode makefile-mode
                                                lua-mode vala-mode)) '("Coding"))
            ((memq major-mode '(nxhtml-mode html-mode
                                            mhtml-mode css-mode)) '("HTML"))
            ((memq major-mode '(org-journal-mode)) '("Journal"))
            ((memq major-mode '(org-mode calendar-mode diary-mode)) '("Org"))
            ((memq major-mode '(dired-mode)) '("Dir"))
            (t '("Other"))))
      (symbol-value 'centaur-tabs-projectile-buffer-group-calc)))

    ;; Don't show tabs for certain types of buffers
    (advice-add 'centaur-tabs-hide-tab :around
                (lambda (oldfn buf &rest args)
                  (if (with-current-buffer buf
                        (or (eq major-mode 'vuiet-mode)
                            (eq major-mode 'dired-sidebar-mode)))
                      t
                    (apply oldfn buf args))))

    ;; Only show tabs in buffers visiting files
    (advice-add 'centaur-tabs-line :around
                (lambda (oldfn &rest args)
                  (if (buffer-file-name)
                      (apply oldfn args)
                    (setq header-line-format nil))))

    ;; Enable prefix argument for tab switching keybindings
    (advice-add 'centaur-tabs-forward :around
                (lambda (oldfn &rest args)
                  (if (numberp current-prefix-arg)
                      (dotimes (_ current-prefix-arg)
                        (apply oldfn args))
                    (apply oldfn args))))
    (advice-add 'centaur-tabs-backward :around
                (lambda (oldfn &rest args)
                  (if (numberp current-prefix-arg)
                      (dotimes (_ current-prefix-arg)
                        (apply oldfn args))
                    (apply oldfn args))))

    ;; Use Org-mode titles for tab names when possible
    (advice-add 'centaur-tabs-buffer-tab-label :around
                (lambda (oldfn tab &rest args)
                  (if-let ((title (or (car
                                       (org-roam-db--get-titles
                                        (buffer-file-name (car tab))))
                                   (org-get-title
                                    (with-current-buffer (car tab)
                                      (buffer-substring (point-min)
                                                        (min (point-max) 200)))))))
                      (if (> centaur-tabs-label-fixed-length 0)
                          (centaur-tabs-truncate-string centaur-tabs-label-fixed-length
                                                        (format " %s" title))
                        (format " %s" title))
                    (apply oldfn tab args))))

    ;; Add a cache to speed up icon rendering for huge groups
    (defvar centaur-tabs-icon-cache (make-hash-table :test 'equal)
      "A cache holding icons generated for centaur-tabs mode tabs.")
    (advice-add 'centaur-tabs-icon :around
                (lambda (oldfn tab face selected &rest args)
                  (let ((key (list tab face selected)))
                    (or (gethash key centaur-tabs-icon-cache)
                        (puthash key
                                 (apply oldfn tab face selected args)
                                 centaur-tabs-icon-cache)))))

    :general
    ((normal motion visual) "g t" #'centaur-tabs-forward)
    ((normal motion visual) "g T" #'centaur-tabs-backward)
    (leader-map "pt" #'centaur-tabs-counsel-switch-group)
    :hook
    (git-commit-mode . (lambda ()
                         (when (centaur-tabs-mode-on-p)
                           (centaur-tabs-local-mode)))))

Frame parameters

Functions to change the frame size:

  (defun jdormit/set-frame-size (width height)
    (interactive "nWidth: \nnHeight: ")
    (if (display-graphic-p)
	(set-frame-size (selected-frame) width height)
      (message "Not running graphically")))

  (defun jdormit/set-frame-width (width)
    (interactive "nWidth: ")
    (jdormit/set-frame-size width (frame-height)))

  (defun jdormit/set-frame-height (height)
    (interactive "nHeight: ")
    (jdormit/set-frame-size (frame-width) height))

Keybindings:

  (leader-def-key "Fw" 'jdormit/set-frame-width)
  (leader-def-key "Fh" 'jdormit/set-frame-height)
  (leader-def-key "Fs" 'jdormit/set-frame-size)

Transpose Frame

A handy utility that reverse the current frame window split (vertical to horizontal or vice-versa):

  (use-package transpose-frame
    :defer t
    :init
    (leader-def-key "wt" 'transpose-frame))

EShell

Easy keybinding to open EShell:

  (defun open-eshell (&optional arg)
    (interactive "P")
    (if (and (fboundp 'projectile-project-root)
	     (projectile-project-root))
	(projectile-run-eshell arg)
      (eshell arg)))

  (leader-def-key "'" 'open-eshell)

Make EShell's tab completion work like Bash's:

  (setq eshell-cmpl-cycle-completions nil)

Add additional useful modules:

  (with-eval-after-load 'eshell
   (add-to-list 'eshell-modules-list 'eshell-tramp))

Prefer Lisp commands to external programs:

  (setq eshell-prefer-lisp-functions t
        eshell-prefer-lisp-variables t)

Enable password caching:

  (setq password-cache t
        password-cache-expiry 120)

Destroy shell buffers created by eshell when the process dies::

  (setq eshell-destroy-buffer-when-process-dies t)

Visual programs:

  (defun eshell-setup ()
    (add-to-list 'eshell-visual-commands "crawl")
    (add-to-list 'eshell-visual-commands "ssh"))

  (add-hook 'eshell-mode-hook #'eshell-setup)

And a function to run any program visually:

  (defun eshell/v (&rest args)
    (apply #'eshell-exec-visual args))

Load .dir-locals.el when switching directories:

  (add-hook 'eshell-mode-hook #'hack-dir-local-variables-non-file-buffer)
  (add-hook 'eshell-after-prompt-hook #'hack-dir-local-variables-non-file-buffer)

A function to properly clear the eshell:

  (defun clear-eshell (&optional prefix)
    (interactive)
    (let ((input (eshell-get-old-input)))
      (eshell/clear-scrollback)
      (eshell-emit-prompt)
      (insert input)))

  (add-hook 'eshell-mode-hook
   (lambda () (general-def eshell-mode-map "C-c C-o" #'clear-eshell)))

Some aliases:

  (setq eshell-aliases
    '(("k" . "kubectl $*")
      ("kctx" . "kubectx $*")
      ("root" . "cd (projectile-project-root)")))
  (add-hook
   'eshell-mode-hook
   (lambda ()
     (dolist (alias eshell-aliases)
       (eshell/alias (car alias) (cdr alias)))))

Prompt

  (defun jdormit-eshell-prompt ()
    (let ((branch (magit-name-local-branch "HEAD")))
     (format "%s%s"
       (if branch (format "(%s) " branch) "")
       (concat (abbreviate-file-name (eshell/pwd))
	 (if (= (user-uid) 0) " # " " $ ")))))

  (setq jdormit-eshell-prompt-regex "^[^#$\n]* [#$] ")

  (setq eshell-prompt-function 'jdormit-eshell-prompt)
  (setq eshell-prompt-regexp jdormit-eshell-prompt-regex)

Flycheck

Syntax checking etc.:

  (use-package flycheck
    :init
    (defhydra hydra-flycheck
      (:pre (flycheck-list-errors)
	    :post (quit-windows-on "*Flycheck errors*")
	    :hint nil)
      "Errors"
      ("f" flycheck-error-list-set-filter "Filter")
      ("j" flycheck-next-error "Next")
      ("k" flycheck-previous-error "Previous")
      ("gg" flycheck-first-error "First")
      ("G" (progn (goto-char (point-max)) (flycheck-previous-error)) "Last")
      ("q" nil))
    :config
    (setq-default flycheck-disabled-checkers '(emacs-lisp emacs-lisp-checkdoc))
    (global-flycheck-mode)
    :general
    ((normal motion visual) flycheck-mode-map "ze" 'hydra-flycheck/body))

Tabs

  (defun disable-tab-insert ()
    (interactive)
    (setq indent-tabs-mode nil))

aggressive-indent-mode

Like electric-indent-mode but reindents after every change:

  (use-package aggressive-indent
    :hook ((clojure-mode . aggressive-indent-mode)
           (emacs-lisp-mode . aggressive-indent-mode)))

JSON

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

  (use-package json-navigator
    :commands (json-navigator-navigator
               json-navigator-navigate-after-point
               json-navigator-navigate-region))

  (use-package tree-mode
    :general
    (normal tree-mode-map
            "D" #'tree-mode-delete-tree
            "k" #'tree-mode-previous-node
            "j" #'tree-mode-next-node
            "l" #'tree-mode-next-sib
            "h" #'tree-mode-previous-sib
            "u" #'tree-mode-goto-parent
            "r" #'tree-mode-goto-root
            "gr" #'tree-mode-reflesh
            "E" #'tree-mode-expand-level
            "e" #'tree-mode-toggle-expand
            "s" #'tree-mode-sort-by-tag
            "/" #'tree-mode-keep-match
            "!" #'tree-mode-collapse-other-except)
    :hook
    ((json-navigator-mode . tree-minor-mode)))

  (defun json-pprint ()
    (interactive)
    (let ((begin (if (region-active-p) (region-beginning) (point-min)))
          (end (if (region-active-p) (region-end) (point-max))))
      (if (executable-find "jq")
          (shell-command-on-region begin end "jq ." nil t)
        (json-pretty-print begin end))))

  (general-def json-mode-map "C-M-\\" 'json-pprint)

JavaScript

Some formatting stuff:

  (setq js-indent-level 4)
  (use-package web-mode
    :mode (("\\.html\\'" . web-mode)
	   ("\\.js\\'" . web-mode)
	   ("\\.jsx\\'" . web-mode)
	   ("\\.mako\\'" . web-mode)
	   ("\\.jinja2\\'" . web-mode)
	   ("\\.hbs\\'" . web-mode))
    :config
    (setq web-mode-engines-alist
	  '(("django" . "\\.jinja2\\'")))
    (add-hook 'web-mode-hook
	      (lambda ()
		(when (equal web-mode-content-type "javascript")
		  (web-mode-set-content-type "jsx"))
		(when (or (equal web-mode-content-type "javascript")
			  (equal web-mode-content-type "jsx"))
		  (lsp-deferred))))
    (add-hook 'web-mode-hook #'disable-tab-insert))

Use nvm to manage node versions:

  (use-package nvm
    :straight ((nvm :host github :repo "rejeep/nvm.el"))
    :commands (nvm-use nvm-use-for nvm-use-for-buffer))

A command to format JS via prettier:

  (defun prettier ()
    (interactive)
    (let ((start (if (use-region-p) (region-beginning) (point-min)))
	  (end (if (use-region-p) (region-end) (point-max)))
	  (parser (cond
		   ((eq major-mode 'graphql-mode) "graphql")
		   (t "babel"))))
      (shell-command-on-region start end (concat "prettier --parser " parser) nil t)))

NVM

Manage node version via NVM within Emacs:

  (use-package nvm
    :commands (nvm-use
	       nvm-use-for
	       nvm-use-for-buffer
	       nvm--installed-versions)
    :init
    (defun nvm (version)
      (interactive (list
		    (completing-read "Node version: "
		     (mapcar #'car
			     (nvm--installed-versions)))))
      (nvm-use version)))

Java

  (use-package lsp-java
    :hook ((java-mode . lsp-deferred)))

Kotlin

  (use-package kotlin-mode
    :mode (("\\.kt\\'" . kotlin-mode))
    :config
    (with-eval-after-load 'lsp
      (when (executable-find "kotlin-language-server")
	(add-hook 'kotlin-mode-hook #'lsp-deferred))))

Groovy

Used for Jenkins configuration scripts and probably other things.

  (use-package groovy-mode
    :commands (groovy-mode)
    :mode (("\\.groovy\\'" . groovy-mode))
    :init
    (add-to-list 'interpreter-mode-alist
		 '("groovy" . groovy-mode))
    :custom
    (groovy-indent-offset 2))

Typescript

  (use-package typescript-mode
    :mode ("\\.ts\\'")
    :config
    (with-eval-after-load 'lsp
      (add-hook 'typescript-mode-hook 'lsp-deferred)))

LSP Mode

Emacs support for the Language Server Protocol

  (use-package lsp-mode
    :defer t
    :init
    (defhydra hydra-lsp (:exit t :hint nil)
      "
   Buffer^^               Server^^                   Symbol
  -------------------------------------------------------------------------------------
   [_f_] format           [_M-r_] restart            [_d_] declaration  [_i_] implementation  [_o_] documentation
   [_m_] imenu            [_S_]   shutdown           [_D_] definition   [_t_] type            [_r_] rename
   [_x_] execute action   [_M-s_] describe session   [_R_] references   [_s_] signature"
      ("d" lsp-find-declaration)
      ("D" lsp-ui-peek-find-definitions)
      ("R" lsp-ui-peek-find-references)
      ("i" lsp-ui-peek-find-implementation)
      ("t" lsp-find-type-definition)
      ("s" lsp-signature-help)
      ("o" lsp-describe-thing-at-point)
      ("r" lsp-rename)

      ("f" lsp-format-buffer)
      ("m" lsp-ui-imenu)
      ("x" lsp-execute-code-action)

      ("M-s" lsp-describe-session)
      ("M-r" lsp-restart-workspace)
      ("S" lsp-shutdown-workspace))
    :general
    (lsp-mode-map "C-c h" 'hydra-lsp/body)
    ((normal visual motion) lsp-mode-map "K" #'lsp-describe-thing-at-point)
    :hook
    ((lsp-mode . (lambda ()
                   (let ((lsp-keymap-prefix "gl"))
                     (lsp-enable-which-key-integration)))))
    :config
    (setq lsp-prefer-flymake nil)
    (general-def '(normal visual motion) "gl" lsp-command-map)
    :commands lsp-mode lsp lsp-deferred
    :custom
    (lsp-enable-snippet nil)
    (lsp-eldoc-render-all nil))

  (use-package lsp-ui
    :after (lsp-mode)
    :custom
    (lsp-ui-sideline-enable t)
    (lsp-ui-sideline-show-symbol t)
    (lsp-ui-sideline-show-hover t)
    (lsp-ui-sideline-show-code-actions t)
    (lsp-ui-sideline-update-mode 'point)
    (lsp-ui-doc-alignment 'window)
    (lsp-ui-doc-header t)
    (lsp-ui-doc-position 'top)
    (lsp-ui-doc-background '((t (:inherit region))))
    (lsp-ui-doc-header '((t (:inherit lsp-face-highlight-write))))
    (lsp-ui-sideline-current-symbol '((t (:inherit font-lock-constant-face
                                                   :weight ultra-bold)))))

  (with-eval-after-load 'lsp-clients
    (defun lsp-typescript-javascript-tsx-jsx-activate-p (filename major-mode)
      "Checks if the javascript-typescript language server should be enabled
    based on FILE-NAME and MAJOR-MODE"
      (or (member major-mode '(typescript-mode typescript-tsx-mode js-mode js2-mode rjsx-mode))
          (and (eq major-mode 'web-mode)
               (or (string-suffix-p ".tsx" filename t)
                   (string-suffix-p ".jsx" filename t)
                   (string-suffix-p ".js" filename t))))))

Python

General

  (leader-def-key "sp" #'run-python)
  (add-hook 'python-mode-hook #'disable-tab-insert)

ISort is a Python utility to sort imports:

  (use-package py-isort
    :commands (py-isort-buffer py-isort-region)
    :config
    (setq py-isort-options '("-m=3"))
    :general
    (python-mode-map "C-c C-i" #'py-isort-buffer))

Run black on the current buffer:

  (general-def 'normal python-mode-map "C-M-\\" #'format-all-buffer)

sphinx-doc.el automatically generates doc strings for Python functions (and updates existing ones!):

  (use-package sphinx-doc
    :hook ((python-mode . sphinx-doc-mode)))

Dev environment/IDE stuff

Support pyvenv within Emacs:

  (use-package pyvenv
    :defer 0
    :commands (pyvenv-mode
               pyvenv-tracking-mode
               pyvenv-workon
               pyvenv-activate
               pyvenv-track-virtualenv)
    :config
    (pyvenv-mode)
    (pyvenv-tracking-mode))

  (defun eshell/workon (name)
    (pyvenv-workon name))

  (defun eshell/activate (dir)
    (pyvenv-activate dir))

  (defun eshell/deactivate ()
    (pyvenv-deactivate))

And support pyenv (NOT pyvenv) to change Python versions:

  (use-package pyenv-mode
    :defer t)

Use the LSP python client:

  (use-package lsp-python-ms
    :init
    (setq lsp-python-ms-auto-install-server t)
    (defun python-lsp ()
      (require 'lsp-python-ms)
      (lsp-deferred))
    :hook
    (python-mode . python-lsp)
    :general
    (python-mode-map "C-c C-d" #'lsp-describe-thing-at-point))

  (general-def 'normal python-mode-map "C-c C-d" #'lsp-describe-thing-at-point)

Override the flycheck python-mypy checker to run in the right directory:

  (flycheck-define-checker python-mypy
    "Mypy syntax and type checker.  Requires mypy>=0.580.

  See URL `http://mypy-lang.org/'."
    :command ("mypy"
              "--show-column-numbers"
              (config-file "--config-file" flycheck-python-mypy-config)
              (option "--cache-dir" flycheck-python-mypy-cache-dir)
              source-original)
    :error-patterns
    ((error line-start (file-name) ":" line (optional ":" column)
            ": error:" (message) line-end)
     (warning line-start (file-name) ":" line (optional ":" column)
              ": warning:" (message) line-end)
     (info line-start (file-name) ":" line (optional ":" column)
           ": note:" (message) line-end))
    :modes python-mode
    ;; Ensure the file is saved, to work around
    ;; https://github.com/python/mypy/issues/4746.
    :predicate flycheck-buffer-saved-p
    :working-directory (lambda (checker)
                         (projectile-compilation-dir)))

Autoflake

Autoflake is a tool that removes unused imports and variables from Python code:

  (defvar autoflake-args '()
    "Arguments to pass to the autoflake command.

  See URL `https://pypi.org/project/autoflake' for options.")

  (defun autoflake (arg)
    (interactive "P")
    (let ((autoflake-cmd (or (executable-find "autoflake")
			     (error "Autoflake executable not found")))
	  (file (cond
		 ((or arg (not buffer-file-name))
		  (read-file-name "Run autoflake on file: "))
		 (buffer-file-name buffer-file-name)
		 (:else (error "Invalid file for autoflake")))))
      (apply #'call-process autoflake-cmd nil nil nil
	     (append autoflake-args (list "--in-place" file)))))

Testing

python-pytest.el integrates Pytest with Emacs:

  (use-package  python-pytest
    :commands (python-pytest-popup
	       python-pytest--current-defun)
    :general
    (python-mode-map "C-c t p" #'python-pytest-popup)
    ((normal motion visual) python-pytest-mode-map "g r" #'python-pytest-repeat)
    ((normal motion visual) python-pytest-mode-map "q" #'quit-window))

And borrowing some functions from python-pytest, we can get some nosetests support as well:

  (defvar nosetests-args ""
    "Additional args to pass to nosetests")

  (defun run-nose (args &optional debug)
    "Runs nosetests with `args'

  If `debug' is non-nil, run in a GUD PDB session."
    (let* ((nosetests-cmd (executable-find "nosetests"))
	   (cmdline (format "%s %s" nosetests-cmd args)))
      (when (not nosetests-cmd)
	(user-error "Nosetests command not found"))
      (if debug
	  (realgud:pdb cmdline)
	(compile cmdline))))

  (defun run-nose-reading-args (arg nose-args &optional debug)
    "Runs nosetests with default args or prompts for args with prefix

  If `debug' is non-nil, run in a GUD PDB session."
    (let ((args (if arg
		    (read-string "Nosetests arguments: "
				 nil nil nosetests-args)
		  nosetests-args)))
      (run-nose (format "%s %s" args nose-args) debug)))

  (defun run-nose-in-project (arg nose-args &optional debug)
    "Runs nosetests from the project root

  If `debug' is non-nil, run in a GUD PDB session."
    (let ((dir (or (projectile-project-root)
		   default-directory)))
      (with-temp-buffer
	(cd dir)
	(run-nose-reading-args arg nose-args debug))))

  (defun nosetests-all (arg)
    "Runs nosetests on all project tests, prompting for the tests directory"
    (interactive "P")
    (let ((test-dir (read-file-name "Test directory: "
				    (or (projectile-project-root)
					default-directory)
				    nil t "tests" #'directory-name-p)))
      (run-nose-in-project arg (directory-file-name test-dir))))

  (defun nosetests-debug-all (arg)
    "Runs nosetests in a GUD session on all project tests"
    (interactive "P")
    (let ((test-dir (read-file-name "Test directory: "
				    (or (projectile-project-root)
					default-directory)
				    nil t "tests" #'directory-name-p)))
      (run-nose-in-project arg (directory-file-name test-dir) t)))

  (defun nosetests-module (arg module)
    "Runs nosetests in the module of the current file"
    (interactive (list current-prefix-arg
		       (read-file-name "Run nosetests on module: "
				       nil nil t
				       (file-name-directory
					(buffer-file-name))
				       #'directory-name-p)))
    (run-nose-in-project arg (directory-file-name module)))

  (defun nosetests-debug-module (arg module)
    "Runs nosetests in a GUD session in the module of the current file"
    (interactive (list current-prefix-arg
		       (read-file-name "Run nosetests on module: "
				       nil nil t
				       (file-name-directory
					(buffer-file-name))
				       #'directory-name-p)))
    (run-nose-in-project arg (directory-file-name module) t))

  (defun nosetests-file (arg file)
    "Runs nosetests on the current file"
    (interactive (list current-prefix-arg
		       (read-file-name "Run nosetests on file: "
				       nil nil t (buffer-file-name))))
    (run-nose-in-project arg file))

  (defun nosetests-debug-file (arg file)
    "Runs nosetests in a GUD session on the current file"
    (interactive (list current-prefix-arg
		       (read-file-name "Run nosetests on file: "
				       nil nil t (buffer-file-name))))
    (run-nose-in-project arg file t))

  (defun nosetests-def (arg def)
    "Runs nosetests on the enclosing function at point"
    (interactive (list current-prefix-arg
		       (python-pytest--current-defun)))
    (run-nose-in-project arg (format "%s:%s"
				     (buffer-file-name)
				     def)))

  (defun nosetests-debug-def (arg def)
    "Runs nosetests in a GUD session on the enclosing function at point"
    (interactive (list current-prefix-arg
		       (python-pytest--current-defun)))
    (run-nose-in-project arg
			 (format "%s:%s"
				 (buffer-file-name)
				 def)
			 t))

  (defvar nosetests-map (make-sparse-keymap)
    "Keymap for nosetests")

  (defvar nosetests-debug-map (make-sparse-keymap)
    "Keymap for debugging nosetests")

  (general-def python-mode-map "C-c t n" nosetests-map)
  (general-def nosetests-map "a" #'nosetests-all)
  (general-def nosetests-map "m" #'nosetests-module)
  (general-def nosetests-map "f" #'nosetests-file)
  (general-def nosetests-map "d" #'nosetests-def)
  (general-def nosetests-map "b" nosetests-debug-map)

  (general-def nosetests-debug-map "a" #'nosetests-debug-all)
  (general-def nosetests-debug-map "m" #'nosetests-debug-module)
  (general-def nosetests-debug-map "f" #'nosetests-debug-file)
  (general-def nosetests-debug-map "d" #'nosetests-debug-def)

  (which-key-add-major-mode-key-based-replacements
    'python-mode "C-c t n" "nosetests")
  (which-key-add-major-mode-key-based-replacements
    'python-mode "C-c t n b" "debug")

Hy

Python but Lispy!

  (defun run-hy ()
    (interactive)
    (run-lisp (expand-file-name "~/.virtualenvs/hy/bin/hy")))

Go

Basic support:

  (use-package go-mode
    :mode (("\\.go\\'" . go-mode)))

LSP support - requires go-langserver.

  (add-hook 'go-mode-hook #'lsp-deferred)

Clojure

Start with clojure-mode:

  (use-package clojure-mode
    :mode (("\\.clj\\'" . clojure-mode)
           ("\\.cljs\\'" . clojurescript-mode)
           ("\\.cljc\\'" . clojurec-mode)
           ("\\.edn\\'" . clojure-mode))
    :config
    (define-clojure-indent
      (defroutes 'defun)
      (GET 2)
      (POST 2)
      (PUT 2)
      (DELETE 2)
      (HEAD 2)
      (ANY 2)
      (OPTIONS 2)
      (PATCH 2)
      (rfn 2)
      (let-routes 1)
      (context 2)
      (:= 3)
      (:+ 3)
      (match 1)))

Add flycheck support:

  (use-package flycheck-clj-kondo
    :after (clojure-mode)
    :if (executable-find "clj-kondo")
    :ensure t)

Sprinkle in some CIDER:

  (use-package cider
    :commands (cider-mode cider-jack-in cider-jack-in-clojurescript)
    :config
    (setq cider-known-endpoints
	  '(("local" "localhost" "4005"))
	  cider-prompt-for-symbol nil)
    (general-def cider-mode-map "C-c t" cider-test-commands-map)
    (add-hook 'cider-repl-mode-hook 'smartparens-strict-mode)
    :hook ((clojure-mode . cider-mode)
	   (clojurescript-mode . cider-mode)
	   (clojurec-mode . cider-mode))
    :general
    (cider-stacktrace-mode-map "SPC" leader-map)
    ('normal cider-mode-map "M-." #'cider-find-var))

  (defun jdormit/cider-setup ()
    (local-set-key (kbd "C-c M-b") 'cider-debug-defun-at-point))

  (add-hook 'cider-mode-hook 'jdormit/cider-setup)

Add some handy hydras:

  (use-package cider-hydra
    :after (cider)
    :hook ((cider-mode . cider-hydra-mode)))

Clj-refactor adds magical refactoring abilities:

  (use-package clj-refactor
    :init
    (defun clj-refactor-setup ()
      (interactive)
      (clj-refactor-mode 1)
      (cljr-add-keybindings-with-prefix "C-c r"))
    :config
    (setq cljr-auto-sort-ns t)
    :hook ((clojure-mode . clj-refactor-setup)))

Add support for running Org-mode Clojure source blocks with Babashka:

  (with-eval-after-load 'ob-clojure
    (defcustom org-babel-clojure-backend nil
      "Backend used to evaluate Clojure code blocks."
      :group 'org-babel
      :type '(choice
              (const :tag "inf-clojure" inf-clojure)
              (const :tag "cider" cider)
              (const :tag "slime" slime)
              (const :tag "bb" bb)
              (const :tag "Not configured yet" nil)))

    (defun elisp->clj (in)
      (cond
       ((listp in) (concat "[" (s-join " " (mapcar #'elisp->clj in)) "]"))
       (t (format "%s" in))))

    (defun ob-clojure-eval-with-bb (expanded params)
      "Evaluate EXPANDED code block with PARAMS using babashka."
      (unless (executable-find "bb")
        (user-error "Babashka not installed"))
      (let* ((stdin (let ((stdin (cdr (assq :stdin params))))
                      (when stdin
                        (elisp->clj
                         (org-babel-ref-resolve stdin)))))
             (input (cdr (assq :input params)))
             (file (make-temp-file "ob-clojure-bb" nil nil expanded))
             (command (concat (when stdin (format "echo %s | " (shell-quote-argument stdin)))
                              (format "bb %s -f %s"
                                      (cond
                                       ((equal input "edn") "")
                                       ((equal input "text") "-i")
                                       (t ""))
                                      (shell-quote-argument file))))
             (result (shell-command-to-string command)))
        (s-trim result)))

    (defun org-babel-execute:clojure (body params)
      "Execute a block of Clojure code with Babel."
      (unless org-babel-clojure-backend
        (user-error "You need to customize org-babel-clojure-backend"))
      (let* ((expanded (org-babel-expand-body:clojure body params))
             (result-params (cdr (assq :result-params params)))
             result)
        (setq result
              (cond
               ((eq org-babel-clojure-backend 'inf-clojure)
                (ob-clojure-eval-with-inf-clojure expanded params))
               ((eq org-babel-clojure-backend 'cider)
                (ob-clojure-eval-with-cider expanded params))
               ((eq org-babel-clojure-backend 'slime)
                (ob-clojure-eval-with-slime expanded params))
               ((eq org-babel-clojure-backend 'bb)
                (ob-clojure-eval-with-bb expanded params))))
        (org-babel-result-cond result-params
          result
          (condition-case nil (org-babel-script-escape result)
            (error result)))))

    (customize-set-variable 'org-babel-clojure-backend 'bb))

  (add-hook 'org-mode-hook (lambda () (require 'ob-clojure)))

Scheme

Tell emacs about file extensions which should activate scheme-mode:

  (add-to-list 'auto-mode-alist '("\\.guile\\'" . scheme-mode))
  (add-to-list 'auto-mode-alist '("\\.rkt\\'" . scheme-mode))

Geiser is a Scheme IDE for Emacs that supports a bunch of common Scheme implementations.

  (use-package geiser
    :commands (run-geiser)
    :general
    (geiser-debug-mode-map "SPC" leader-map))

And a handy shortcut to hop into a Geiser REPL:

  (leader-def-key "sg" 'run-geiser)

Common Lisp

SLIME is a set of modes and utilities for writing Common Lisp in Emacs. Quicklisp is the de-facto Common Lisp package manager. It comes with some Emacs bindings.

  (use-package slime-company
    :after (slime))

  (use-package slime
    :commands (slime)
    :config
    (setq inferior-lisp-program
	  (executable-find "sbcl")
	  slime-contribs '(slime-repl
			   slime-fancy
			   slime-company))
    (when (file-exists-p
	   (expand-file-name "~/quicklisp/slime-helper.el"))
      (load (expand-file-name "~/quicklisp/slime-helper.el")))
    (add-hook 'slime-repl-mode-hook 'smartparens-strict-mode)
    :general
    ((normal motion visual insert emacs) slime-mode-map "M-." #'slime-edit-definition))

    (add-to-list 'auto-mode-alist '("\\.cl\\'" . lisp-mode))

Keyboard shortcut to start a SLIME REPL:

  (leader-def-key "sc" 'slime)

Haskell

  (defun jdormit/haskell-setup ()
    (local-set-key (kbd "C-c M-j") 'interactive-haskell-mode))
  (use-package haskell-mode
    :mode (("\\.hs\\'" . haskell-mode)))
  (add-hook 'haskell-mode-hook 'jdormit/haskell-setup)

PHP

  (use-package php-mode
    :mode "\\.php\\'")

  (use-package mmm-mode
    :after php-mode)

Geben is an interface to XDebug allowing debugging PHP in Emacs:

  (use-package geben
    :commands (geben))

Some keybindings to start and end Geben:

  (general-def php-mode-map "C-c C-d" #'geben)
  (general-def php-mode-map "C-c C-q" #'geben-end)

An Eshell alias to start PHP using XDebug:

  (add-hook 'eshell-mode-hook
	    (lambda ()
	      (eshell/alias "php-debug"
			    "php -d xdebug.remote_enable=on -d xdebug.remote_host=127.0.0.1 -d xdebug.remote_port=9000 -d xdebug.remote_handler=dbgp -d xdebug.idekey=geben -d xdebug.remote_autostart=On $*")))

LSP for PHP requires php-language-server to be installed in ~/.composer:

  (add-hook 'php-mode-hook #'lsp-deferred)

YAML

  (use-package yaml-mode
    :mode ("//.yml//'"))

Pharen

Pharen is a Lisp that compiles to PHP. It looks a lot like Clojure.

  (add-to-list 'auto-mode-alist '("\\.phn\\'" . clojure-mode))

Bash

Use LSP if bash-language-server is installed.

  (when (executable-find "bash-language-server")
    (add-hook 'sh-mode-hook #'lsp-deferred))

Ruby

  (add-hook 'ruby-mode-hook #'lsp-deferred)

Rust

  (use-package rust-mode
    :mode "\\.rs\\'"
    :general
    (rust-mode-map "C-c <tab>" #'rust-format-buffer)
    :config
    (add-hook 'rust-mode-hook #'lsp-deferred))


  (use-package cargo
    :after (rust-mode)
    :config
    (add-hook 'rust-mode-hook #'cargo-minor-mode))

XML

Set up hideshow for nXML mode:

  (add-hook 'nxml-mode-hook #'hs-minor-mode)

  (add-to-list 'hs-special-modes-alist
	       '(nxml-mode
		 "<!--\\|<[^/>]*[^/]>" ;; regexp for start block
		 "-->\\|</[^/>]*[^/]>" ;; regexp for end block
		 "<!--"
		 nxml-forward-element
		 nil))

A function to format XML using tidy or xmllint if available, falling back to sgml-pretty-print:

  (defun xml-pretty-print ()
    (interactive)
    (let ((start (if (region-active-p) (region-beginning) (point-min)))
	  (end (if (region-active-p) (region-end) (point-max))))
      (cond
       ((executable-find "tidy")
	(shell-command-on-region start end "tidy -wrap 88 -q -i -xml" nil t))
       ((executable-find "xmllint")
	(shell-command-on-region start end "xmllint --format -" nil t))
       (t (sgml-pretty-print start end)))))

  (general-def nxml-mode-map "C-M-\\" #'xml-pretty-print)

CSVs

  (use-package csv-mode
    :mode "\\.csv\\'")

Markdown

  (use-package markdown-mode
    :commands (markdown-mode gfm-mode)
    :mode (("README\\.md\\'" . gfm-mode)
           ("\\.md\\'" . markdown-mode)
           ("\\.markdown\\'" . markdown-mode))
    :init (setq markdown-command "pandoc"))

Edit-indirect allows markdown-mode to edit source blocks in separate buffers:

  (use-package edit-indirect
    :defer t)

IELM

Enable lexical scoping in IELM:

  (add-hook 'ielm-mode-hook
            #'(lambda ()
                (interactive)
                (setq lexical-binding t)))

Ledger Mode

This mode requires that ledger be installed on the system.

  (use-package ledger-mode
    :mode "\\.ledger\\'"
    :config
    (add-to-list 'evil-emacs-state-modes 'ledger-report-mode)
    (general-def ledger-report-mode-map "SPC" leader-map)
    (add-hook 'ledger-mode-hook
	      (lambda ()
		(variable-pitch-mode 0)))
    (add-hook 'ledger-report-mode-hook
	      (lambda ()
		(variable-pitch-mode 0))))

Importing

  (defvar ledger-file (expand-file-name "~/journal.ledger"))

  (defvar bank-alist
    '(("DCU Checking" . ((acct . "Assets:Checking")
			 (fid . "9999")))
      ("DCU Savings" . ((acct . "Assets:Savings")
			(fid . "9999")))
      ("DCU Visa" . ((acct . "Liabilities:DCU Visa")
		     (fid . "9999")))
      ("Chase Visa" . ((acct . "Liabilities:Chase Visa")))))

  (defun ledger-import-ofx (bank file)
    (interactive
     (list
      (completing-read "Bank: "
			    (mapcar #'car bank-alist))
      (read-file-name "OFX file: ")))
    (if-let ((ledger-autosync (executable-find "ledger-autosync")))
	(let* ((bank-def (alist-get bank bank-alist))
	       (acct (alist-get 'acct bank-def))
	       (fid (alist-get 'fid bank-def))
	       (cmd (concat
		     ledger-autosync
		     (if fid (concat " --fid " fid) "")
		     " --account '" acct "'"
		     " '" file "'"))
	       (output (shell-command-to-string cmd)))
	  (find-file ledger-file)
	  (goto-char (point-max))
	  (insert "\n")
	  (insert output)
	  (ledger-sort-region (point-min) (point-max))
	  (ledger-post-align-postings (point-min) (point-max)))
      (error "Unable to find ledger-autosync")))

PDFs

  (use-package pdf-tools
    :mode ("\\.pdf\\'" . pdf-view-mode)
    :init
    (defhydra hydra-pdftools (:color blue :hint nil)
      "
                                                                        ╭───────────┐
         Move  History   Scale/Fit     Annotations  Search/Link    Do   │ PDF Tools │
     ╭──────────────────────────────────────────────────────────────────┴───────────╯
           ^^_g_^^      _B_    ^↧^    _+_    ^ ^     [_al_] list    [_s_] search    [_u_] revert buffer
           ^^^↑^^^      ^↑^    _H_    ^↑^  ↦ _W_ ↤   [_am_] markup  [_o_] outline   [_i_] info
           ^^_p_^^      ^ ^    ^↥^    _0_    ^ ^     [_at_] text    [_F_] link      [_d_] dark mode
           ^^^↑^^^      ^↓^  ╭─^─^─┐  ^↓^  ╭─^ ^─┐   [_ad_] delete  [_f_] search link
      _h_ ←pag_e_→ _l_  _N_  │ _P_ │  _-_    _b_     [_aa_] dired
           ^^^↓^^^      ^ ^  ╰─^─^─╯  ^ ^  ╰─^ ^─╯   [_y_]  yank
           ^^_n_^^      ^ ^  _r_eset slice box
           ^^^↓^^^
           ^^_G_^^
     --------------------------------------------------------------------------------
          "
      ("\\" hydra-master/body "back")
      ("<ESC>" nil "quit")
      ("al" pdf-annot-list-annotations)
      ("ad" pdf-annot-delete)
      ("aa" pdf-annot-attachment-dired)
      ("am" pdf-annot-add-markup-annotation)
      ("at" pdf-annot-add-text-annotation)
      ("y"  pdf-view-kill-ring-save)
      ("+" pdf-view-enlarge :color red)
      ("-" pdf-view-shrink :color red)
      ("0" pdf-view-scale-reset)
      ("H" pdf-view-fit-height-to-window)
      ("W" pdf-view-fit-width-to-window)
      ("P" pdf-view-fit-page-to-window)
      ("n" pdf-view-next-page-command :color red)
      ("p" pdf-view-previous-page-command :color red)
      ("d" pdf-view-dark-minor-mode)
      ("b" pdf-view-set-slice-from-bounding-box)
      ("r" pdf-view-reset-slice)
      ("g" pdf-view-first-page)
      ("G" pdf-view-last-page)
      ("e" pdf-view-goto-page)
      ("o" pdf-outline)
      ("s" pdf-occur)
      ("i" pdf-misc-display-metadata)
      ("u" pdf-view-revert-buffer)
      ("F" pdf-links-action-perform)
      ("f" pdf-links-isearch-link)
      ("B" pdf-history-backward :color red)
      ("N" pdf-history-forward :color red)
      ("l" image-forward-hscroll :color red)
      ("h" image-backward-hscroll :color red))
    :config
    (pdf-tools-install)
    :general
    (pdf-view-mode-map "SPC" leader-map)
    ((normal motion visual) pdf-view-mode-map "." #'hydra-pdftools/body)
    ((normal motion visual) pdf-view-mode-map "F" #'pdf-links-action-perform))

EPubs

  (defun jdormit/nov-config ()
    (olivetti-mode)
    (nov-render-document))

  (use-package nov
    :mode ("\\.epub\\'" . nov-mode)
    :config
    (setq nov-text-width 80)
    (add-hook 'nov-mode-hook 'jdormit/nov-config)
    :general
    ('normal nov-mode-map "r" 'nov-render-document)
    ('normal nov-mode-map "=" 'nov-view-source)
    ('normal nov-mode-map "+" 'nov-view-content-source)
    ('normal nov-mode-map "m" 'nov-display-metadata)
    ('normal nov-mode-map "n" 'nov-next-document)
    ('normal nov-mode-map "]" 'nov-next-document)
    ('normal nov-mode-map "p" 'nov-previous-document)
    ('normal nov-mode-map "[" 'nov-previous-document)
    ('normal nov-mode-map "t" 'nov-goto-toc)
    ('normal nov-mode-map "RET" 'nov-browse-url)
    ('normal nov-mode-map "<follow-link>" 'mouse-face)
    ('normal nov-mode-map "<mouse-2>" 'nov-browse-url)
    ('normal nov-mode-map "TAB" 'shr-next-link)
    ('normal nov-mode-map "M-TAB" 'shr-previous-link)
    ('normal nov-mode-map "<backtab>" 'shr-previous-link)
    ('normal nov-mode-map "SPC" 'nov-scroll-up)
    ('normal nov-mode-map "S-SPC" 'nov-scroll-down)
    ('normal nov-mode-map "DEL" 'nov-scroll-down)
    ('normal nov-mode-map "<home>" 'beginning-of-buffer)
    ('normal nov-mode-map "<end>" 'end-of-buffer)
    ('normal nov-mode-map "SPC" leader-map))

Org mode links

First, keep a reference to filename of the .epub before nov.el blows it away:

  (defvar nov-epub-file)
  (add-hook 'nov-mode-hook
            #'(lambda ()
                (message "epub file: " buffer-file-name)
                (setq nov-epub-file buffer-file-name)))

That reference lets us construct a link back to the .epub:

  (defun org-epub-store-link ()
    (when (eq major-mode 'nov-mode)
      (let ((epub-name (alist-get 'title nov-metadata)))
        (org-store-link-props
         :type "file"
         :link (concat "file://" nov-epub-file)))))

  (add-hook 'org-store-link-functions 'org-epub-store-link)

Dashboard

Instead of the GNU Emacs buffer on startup, display a cool dashboard:

  (use-package dashboard
    :general
    (dashboard-mode-map "SPC" leader-map)
    :config
    (with-eval-after-load 'evil
      (add-to-list 'evil-emacs-state-modes 'dashboard-mode))
    (setq dashboard-items '((recents . 5)
			    (projects . 5))
	  dashboard-startup-banner 'official
	  dashboard-set-heading-icons t
	  dashboard-set-file-icons t
	  dashboard-set-navigator t))

  (dashboard-setup-startup-hook)

For some reason Emacs is starting up with the dashboard and the scratch buffer open in a split configuration. Not sure why, but let's put a stop to that…

  (add-hook
   'after-init-hook
   (lambda ()
     (switch-to-buffer "*dashboard*")
     (delete-other-windows)))

Email

I use mu/mu4e as my email client.

First, add it to the load path:

  (defvar mu4e-load-path)
  (setq mu4e-load-path
        (cond
         ((file-exists-p "/usr/local/share/emacs/site-lisp/mu/mu4e")
          "/usr/local/share/emacs/site-lisp/mu/mu4e")
         ((file-exists-p "/usr/share/emacs/site-lisp/mu4e")
          "/usr/share/emacs/site-lisp/mu4e")))
  (add-to-list 'load-path mu4e-load-path)

Then set up autoloads on the entry functions:

  (autoload 'mu4e (concat mu4e-load-path "/mu4e.el") nil t)
  (autoload 'mu4e-update-index (concat mu4e-load-path "/mu4e.el") nil t)

Then configure it:

  (with-eval-after-load 'mu4e
    (setq
     ;; General
     mu4e-maildir (expand-file-name "~/.mail")
     mu4e-completing-read-function 'completing-read
     mu4e-attachment-dir (expand-file-name "~/Downloads")
     mu4e-change-filenames-when-moving t
     user-mail-address "jeremy.dormitzer@gmail.com"
     mu4e-view-show-images t
     mu4e-headers-skip-duplicates t
     mail-user-agent 'mu4e-user-agent
     ;; Custom actions
     mu4e-view-actions '(("capture message" . mu4e-action-capture-message)
                         ("view as pdf" . mu4e-action-view-as-pdf)
                         ("show this thread" . mu4e-action-show-thread)
                         ("View in browser" . mu4e-action-view-in-browser))
     ;; Bookmarked searches
     mu4e-bookmarks '((:name "Inbox"
                             :query (concat "maildir:/jeremy-dormitzer-gmail-com/Inbox"
                                            " OR maildir:/jeremydormitzer-lola-com/Inbox")
                             :key ?i)
                      (:name "Unread messages"
                             :query "flag:unread AND NOT flag:trashed"
                             :key ?u)
                      (:name "Today's messages"
                             :query "date:today..now"
                             :key ?t)
                      (:name "Last 7 days"
                             :query "date:7d..now"
                             :key ?p))
     ;; Getting mail
     mu4e-get-mail-command "mbsync -a"
     ;; Sending mail
     send-mail-function #'sendmail-send-it
     message-send-mail-function #'sendmail-send-it
     sendmail-program (executable-find "msmtp")
     ;; Let Gmail handle putting sent messages in the sent folder
     mu4e-sent-messages-behavior 'delete
     ;; Move to trash folder instead of adding trash flag for Gmail mailboxes
     mu4e-move-to-trash-patterns '("jeremy-dormitzer-gmail-com" "jeremydormitzer-lola-com")
     ;; HTML email rendering
     shr-use-colors nil
     ;; Make sure mu4e knows about my different accounts
     mu4e-context-policy 'ask
     mu4e-compose-context-policy 'ask
     mu4e-contexts
     `(,(make-mu4e-context
         :name "Lola Gmail"
         :match-func (lambda (msg)
                       (when msg
                         (or
                          (mu4e-message-contact-field-matches msg
                                                              :to
                                                              "lola.com")
                          (string-match-p
                           "jeremydormitzer-lola-com"
                           (mu4e-message-field msg :path)))))
         :vars '((user-mail-address . "jeremydormitzer@lola.com")
                 (mu4e-compose-signature . "Best regards,  \nJeremy Dormitzer  \nLola.com")
                 (message-signature-insert-empty-line . t)
                 (mu4e-sent-folder . "/jeremydormitzer-lola-com/Sent")
                 (mu4e-drafts-folder . "/jeremydormitzer-lola-com/Drafts")
                 (mu4e-refile-folder . "/jeremydormitzer-lola-com/Archive")
                 (mu4e-trash-folder . "/jeremydormitzer-lola-com/Trash")
                 (message-sendmail-extra-arguments
                  . ("-a" "jeremydormitzer-lola-com"))))
       ,(make-mu4e-context
         :name "Personal Gmail"
         :match-func (lambda (msg)
                       (when msg
                         (string-match-p
                          "jeremy-dormitzer-gmail-com"
                          (mu4e-message-field msg :path))))
         :vars '((user-mail-address . "jeremy.dormitzer@gmail.com")
                 (mu4e-sent-folder . "/jeremy-dormitzer-gmail-com/Sent")
                 (mu4e-drafts-folder . "/jeremy-dormitzer-gmail-com/Drafts")
                 (mu4e-refile-folder . "/jeremy-dormitzer-gmail-com/Archive")
                 (mu4e-trash-folder . "/jeremy-dormitzer-gmail-com/Trash")
                 (message-sendmail-extra-arguments
                  . ("-a" "jeremy-dormitzer-gmail-com"))))))

    ;; Custom mark function to mark messages matching the current message
    (defun mu4e-mark-matching-pred (msg from)
      (mu4e-message-contact-field-matches msg :from from))

    (defun mu4e-mark-matching-input ()
      (let* ((msg (mu4e-message-at-point t)))
        (if (not msg)
            (error "No message at point")
          (cdr (mu4e-message-field msg :from)))))

    (setq mu4e-headers-custom-markers
          '(("Older than"
             (lambda
               (msg date)
               (time-less-p
                (mu4e-msg-field msg :date)
                date))
             (lambda nil
               (mu4e-get-time-date "Match messages before: ")))
            ("Newer than"
             (lambda
               (msg date)
               (time-less-p date
                            (mu4e-msg-field msg :date)))
             (lambda nil
               (mu4e-get-time-date "Match messages after: ")))
            ("Bigger than"
             (lambda
               (msg bytes)
               (>
                (mu4e-msg-field msg :size)
                (* 1024 bytes)))
             (lambda nil
               (read-number "Match messages bigger than (Kbytes): ")))
            ("Matching current message from: field"
             (lambda (msg from)
               (mu4e-message-contact-field-matches msg :from from))
             (lambda ()
               (let* ((msg (mu4e-message-at-point t)))
                 (if (not msg)
                     (error "No message at point")
                   (cdar (mu4e-message-field msg :from))))))))
    (add-hook 'mu4e-compose-pre-hook
              (lambda ()
                (set
                 (make-local-variable '*should-delete-trailing-whitespace*)
                 nil))))

Support sending attachments from Dired:

  (with-eval-after-load 'dired
    (require 'gnus-dired)
    ;; make the `gnus-dired-mail-buffers' function also work on
    ;; message-mode derived modes, such as mu4e-compose-mode
    (defun gnus-dired-mail-buffers ()
      "Return a list of active message buffers."
      (let (buffers)
        (save-current-buffer
          (dolist (buffer (buffer-list t))
            (set-buffer buffer)
            (when (and (derived-mode-p 'message-mode)
                       (null message-sent-message-via))
              (push (buffer-name buffer) buffers))))
        (nreverse buffers)))

    (setq gnus-dired-mail-mode 'mu4e-user-agent)
    (add-hook 'dired-mode-hook 'turn-on-gnus-dired-mode))

Support sending rich-text emails via Markdown:

  (defvar *message-md-pandoc-html-template*
    "<html>
    <head>
      <meta charset=\"utf-8\" />
    </head>
    <body>
    $body$
    </body>
  </html>"
    "The default template used when converting markdown to HTML via pandoc.")

  (defun gfm->html (gfm &optional template)
    "Converts GitHub-flavored markdown to HTML via pandoc.

  By default, the template `*message-md-pandoc-html-template*' is used,
  but this can be overridden with the TEMPLATE argument."
    (unless (executable-find "pandoc")
      (error "Pandoc not found, unable to convert"))
    (let ((template-file (make-temp-file "gfm->html-template"
                                         nil ".html"
                                         (or template *message-md-pandoc-html-template*))))
      (with-temp-buffer
        (insert gfm)
        (unless
            (= 0
               (call-process-region (point-min) (point-max) "pandoc" t t nil
                                    "--template" template-file "--quiet"
                                    "-f" "gfm" "-t" "html"))
          (error "Markdown to HTML conversion failed: %s"
                 (buffer-substring (point-min) (point-max))))
        (buffer-substring (point-min) (point-max)))))

  (defun mml-node->str (node)
    "Converts a parsed MML node back to an MML string."
    (let ((node-name (car node))
          (node-alist (cdr node)))
      (format "<#%s%s>\n%s"
              node-name
              (cl-reduce (lambda (acc pair)
                           (if (equal (car pair) 'contents)
                               acc
                             (concat acc (format " %s=%S"
                                                 (car pair)
                                                 (cdr pair)))))
                         node-alist
                         :initial-value "")
              (cdr (assoc 'contents node-alist))
              node-name)))

  (defun multipart-message-as-string (plain html inline attachments)
    "Given MML nodes PLAIN, and HTML and MML node lists INLINE and
  ATTACHMENTS, constructs a multipart MML email and returns it as a
  string."
    (let* ((alternative (concat "<#multipart type=alternative>\n"
                                (mml-node->str plain) "\n"
                                (mml-node->str html) "\n"
                                "<#/multipart>"))
           (related (when inline
                      (concat "<#multipart type=related>\n"
                              alternative "\n"
                              (cl-reduce
                               (lambda (acc node)
                                 (concat acc (mml-node->str node) "\n"))
                               inline
                               :initial-value "")
                              "<#/multipart>")))
           (mixed (when attachments
                    (concat "<#multipart type=mixed>\n"
                            (or related alternative) "\n"
                            (cl-reduce
                             (lambda (acc node)
                               (concat acc (mml-node->str node) "\n"))
                             attachments
                             :initial-value "")
                            "<#/multipart>"))))
      (or mixed related alternative)))

  (defun assoc-mml-node (key node)
    (cdr (assoc key (cdr node))))

  (defun multipart-html-message (raw)
    "Creates a multipart HTML email with a text part and an html part."
    (with-temp-buffer
      (insert raw)
      (let* ((parsed (mml-parse))
             (plain (cl-reduce
                     (lambda (acc node)
                       (if (not (equal (assoc-mml-node 'type node) "text/plain"))
                           acc
                         `(part (type . "text/plain")
                                (contents . ,(concat (cdaddr acc)
                                                     (cdaddr node))))))
                     parsed
                     :initial-value '(part (type . "text/plain")
                                           (contents . ""))))
             (html `(part (type . "text/html")
                          (contents . ,(gfm->html (cdaddr plain)))))
             (inline (nreverse
                      (cl-reduce
                       (lambda (acc node)
                         (if (not (equal (assoc-mml-node 'disposition node)
                                         "inline"))
                             acc
                           (cons node acc)))
                       parsed
                       :initial-value nil)))
             (attachments (nreverse
                           (cl-reduce
                            (lambda (acc node)
                              (if (not (equal (assoc-mml-node 'disposition node)
                                              "attachment"))
                                  acc
                                (cons node acc)))
                            parsed
                            :initial-value nil))))
        (multipart-message-as-string plain
                                     html
                                     inline
                                     attachments))))

  (defun convert-message-to-markdown ()
    "Convert the message in the current buffer to a multipart HTML email.

  The HTML is rendered by treating the message content as Markdown."
    (interactive)
    (let* ((begin
            (save-excursion
              (goto-char (point-min))
              (search-forward mail-header-separator)))
           (end (point-max))
           (raw-body (buffer-substring begin end)))
      (undo-boundary)
      (delete-region begin end)
      (save-excursion
        (goto-char begin)
        (newline)
        (insert (multipart-html-message raw-body)))))

  (defun message-md-send (&optional arg)
    "Convert the current buffer and send it.
  If given prefix arg ARG, skips markdown conversion."
    (interactive "P")
    (unless arg
      (convert-message-to-markdown))
    (message-send))

  (defun message-md-send-and-exit (&optional arg)
    "Convert the current buffer and send it, then exit from mail buffer.
  If given prefix arg ARG, skips markdown conversion."
    (interactive "P")
    (unless arg
      (convert-message-to-markdown))
    (message-send-and-exit))

  (with-eval-after-load 'message
    (define-key message-mode-map (kbd "C-c C-s") #'message-md-send)
    (define-key message-mode-map (kbd "C-c C-c") #'message-md-send-and-exit))

  ;; Handle replies to HTML emails as well
  (defun html->gfm (html)
    (unless (executable-find "pandoc")
      (error "Pandoc not found, unable to convert"))
    (with-temp-buffer
      (insert html)
      (unless
          (= 0
             (call-process-region (point-min) (point-max) "pandoc" t t nil
                                  "--quiet"
                                  "-f" "html-native_divs-native_spans"
                                  "-t" "gfm"))
        (error "HTML to mardkwon conversion failed: %s"
               (buffer-substring (point-min) (point-max))))
      (buffer-substring (point-min) (point-max))))

  (with-eval-after-load 'mu4e
    (defvar message-md-rich-text-reply t)
    (defvar message-md--inhibit-rich-text-reply nil)

    (defun html-to-md-command (msg)
      "Returns the text of MSG as Markdown."
      (if (mu4e-msg-field msg :body-html)
          (html->gfm (mu4e-msg-field msg :body-html))
        (mu4e-msg-field msg :body-txt)))

    (defun mu4e-draft-cite-original-advice (oldfn &rest args)
      (let ((res (if (and message-md-rich-text-reply
                          (not message-md--inhibit-rich-text-reply))
                     (let ((mu4e-view-prefer-html t)
                           (mu4e-html2text-command #'html-to-md-command))
                       (apply oldfn args))
                   (apply oldfn args))))
        (setq message-md--inhibit-rich-text-reply nil)
        res))

    (defun mu4e-compose-reply-advice (oldfn &rest args)
      (when current-prefix-arg
        (setq message-md--inhibit-rich-text-reply t))
      (apply oldfn args))

    (advice-add 'mu4e~draft-cite-original :around #'mu4e-draft-cite-original-advice)
    (advice-add 'mu4e-compose-reply :around #'mu4e-compose-reply-advice))

  ;; Add an "X-Attachment-Id" header to MIME stuff as well as a Content-Id:
  (with-eval-after-load 'mml
    (defun mml-insert-mime-headers (cont type charset encoding flowed)
      (let (parameters id disposition description)
        (setq parameters
              (mml-parameter-string
               cont mml-content-type-parameters))
        (when (or charset
                  parameters
                  flowed
                  (not (equal type mml-generate-default-type))
                  mml-insert-mime-headers-always)
          (when (consp charset)
            (error
             "Can't encode a part with several charsets"))
          (insert "Content-Type: " type)
          (when charset
            (mml-insert-parameter
             (mail-header-encode-parameter "charset" (symbol-name charset))))
          (when flowed
            (mml-insert-parameter "format=flowed"))
          (when parameters
            (mml-insert-parameter-string
             cont mml-content-type-parameters))
          (insert "\n"))
        (when (setq id (cdr (assq 'id cont)))
          (insert "Content-ID: " id "\n"))
        (when (setq x-attachment-id (cdr (assq 'x-attachment-id cont)))
          (insert "X-Attachment-Id: " x-attachment-id "\n"))
        (setq parameters
              (mml-parameter-string
               cont mml-content-disposition-parameters))
        (when (or (setq disposition (cdr (assq 'disposition cont)))
                  parameters)
          (insert "Content-Disposition: "
                  (or disposition
                      (mml-content-disposition type (cdr (assq 'filename cont)))))
          (when parameters
            (mml-insert-parameter-string
             cont mml-content-disposition-parameters))
          (insert "\n"))
        (unless (eq encoding '7bit)
          (insert (format "Content-Transfer-Encoding: %s\n" encoding)))
        (when (setq description (cdr (assq 'description cont)))
          (insert "Content-Description: ")
          (setq description (prog1
                                (point)
                              (insert description "\n")))
          (mail-encode-encoded-word-region description (point))))))

  (defun message-md-insert-inline-image (image type description)
    (interactive
     (let* ((file (mml-minibuffer-read-file "Insert image: "))
            (type (if current-prefix-arg
                      (or (mm-default-file-encoding file)
                          "application/octet-stream")
                    (mml-minibuffer-read-type file)))
            (description (if current-prefix-arg
                             nil
                           (mml-minibuffer-read-description))))
       (list file type description)))
    (let ((id (format "%s%s" (s-snake-case description) (random))))
     (insert (format "![%s](cid:%s)" description id))
     (save-excursion
       (goto-char (point-max))
       (newline)
       (insert
        (format (concat "<#part id=\"<%s>\" x-attachment-id=%S "
                        "type=%S filename=%S disposition=inline description=%s>")
                id id type image description)))))


  ;; The default message signature begins with "-- \n". To makes this
  ;; markdown compatible, we need to add an extra space at the end: "--  \n"
  (defun message-md-add-space-to-sig-separator (&rest args)
    (save-excursion
      (when (search-backward "--" nil t)
        (forward-char 2)
        (insert " "))))

  (advice-add 'message-insert-signature :after
              #'message-md-add-space-to-sig-separator)

Global keybindings:

  (leader-def-key "am" #'mu4e)

Keybindings within mu4e:

  (with-eval-after-load 'mu4e
    (general-def '(normal motion insert emacs) mu4e-headers-mode-map "t" #'mu4e-headers-mark-thread)
    (general-def '(normal motion insert emacs) mu4e-view-mode-map "t" #'mu4e-view-mark-thread))

Mu4e uses shr to render HTML emails. Unfortunately the shr function that sets faces in the rendered document has a bug: it appends the shr faces to the existing face-list, rather than prepending the shr face. This means that e.g. links don't actually get rendered correctly if there is some non-link face already on the text. The fix:

  (with-eval-after-load 'shr
   (defun shr-add-font (start end type)
     (save-excursion
       (goto-char start)
       (while (< (point) end)
	 (when (bolp)
	   (skip-chars-forward " "))
	 ;; Remove the APPEND argument to add-face-text-property
	 ;; so the face ends up at the head of the face list
	 (add-face-text-property (point) (min (line-end-position) end) type)
	 (if (< (line-end-position) end)
	     (forward-line 1)
	   (goto-char end))))))

w3m

Browsing the web from Emacs. Relies on having w3m installed.

  (use-package w3m
    :commands (w3m
               w3m-browse-url
               w3m-search-new-session)
    :init
    (setq w3m-home-page "https://start.duckduckgo.com"
          w3m-search-default-engine "duckduckgo"
          w3m-cookie-reject-domains '("www.wsj.com"
                                      "www.bbc.com"
                                      "www.nytimes.com"
                                      "www.washingtonpost.com")
          w3m-use-tab-line nil
          browse-url-browser-function '(("nytimes.com" . w3m-browse-url)
                                        ("wsj.com" . w3m-browse-url)
                                        ("." . browse-url-default-browser)))
    :general
    ('normal w3m-mode-map "SPC" leader-map)
    ('(normal visual motion) w3m-mode-map "C-f" #'w3m-scroll-up-or-next-url)
    ('(normal visual motion) w3m-mode-map "C-b" #'w3m-scroll-down-or-previous-url)
    ('normal w3m-mode-map "J" #'w3m-previous-buffer)
    ('normal w3m-mode-map "K" #'w3m-next-buffer)
    ('normal w3m-mode-map "gs" #'w3m-search)
    ('normal w3m-mode-map "gS" #'w3m-search-new-session)
    ('normal w3m-mode-map "gl" #'link-hint-open-link)
    ('normal w3m-mode-map "gc" #'link-hint-copy-link)
    ('normal w3m-mode-map "zc" #'w3m-print-current-url))

    (jdormit/define-prefix "aw" "w3m")
    (leader-def-key "aww" 'w3m)
    (leader-def-key "aws" 'w3m-search-new-session)
    (leader-def-key "awb" 'w3m-browse-url)

I mostly want `browse-url-at-point` to open stuff in Firefox, but in some cases I want it within Emacs:

  (defun browse-url-at-point-w3m ()
    "Opens the URL at point in w3m"
    (interactive)
    (let ((browse-url-browser-function 'w3m-browse-url))
      (if (eq major-mode 'org-mode)
          (org-open-at-point)
          (browse-url-at-point))))

  (leader-def-key "awB" 'browse-url-at-point-w3m)

I want to be able to set a custom "Referer" header by setting the variable `jdormit/w3m-referers`. I also want to be able to set websites that cookies will never get sent to:

  (defvar jdormit/w3m-referers nil)
  (defvar jdormit/w3m-no-cookie-sites nil)

  (defun get-referer (url referer-list)
    "Retrieve the referer specified by url in referer-list"
    (when (not (eq nil referer-list))
      (let ((first (car referer-list))
            (rest (cdr referer-list)))
        (if (string-match-p (car first) url)
            (cdr first)
          (get-referer url rest)))))

  (defun should-not-set-cookie-p (url no-cookie-sites)
    "Non-nil if cookies should not be sent to url"
    (when (not (eq nil no-cookie-sites))
      (if (string-match-p (car no-cookie-sites) url)
          t
        (should-not-set-cookie-p url (cdr no-cookie-sites)))))

  (advice-add 'w3m-header-arguments :around
              (lambda (w3m-header-arguments &rest r)
                (cl-destructuring-bind
                    (method url temp-file body referer content-type) r
                  (let ((w3m-use-cookies
                         (if (should-not-set-cookie-p url jdormit/w3m-no-cookie-sites)
                             nil
                           w3m-use-cookies)))
                    (if-let ((referer (get-referer url jdormit/w3m-referers)))
                              (funcall w3m-header-arguments
                                       method
                                       url
                                       temp-file
                                       body
                                       referer
                                       content-type)
                            (apply w3m-header-arguments r))))))

And here are the websites where I want custom referers and/or no cookies:

  (setq jdormit/w3m-referers '(("www.wsj.com" . "https://google.com")
			       ("www.nytimes.com" . "https://google.com")
			       ("www.newyorker.com" . "https://google.com")
			       ("www.economist.com" . "https://google.com")))
  (setq jdormit/w3m-no-cookie-sites '("www.wsj.com"
				      "www.nytimes.com"
				      "www.economist.com"
				      "www.newyorker.com"))

Render <code> and <pre> blocks in a different face. Which face can be set by setting or customizing the variable w3m-code-block-face. It defaults to 'fixed-pitch:

  (setq w3m-code-open-delimiter "{{{W3M_CODE_BLOCK_BEGIN}}}")
  (setq w3m-code-close-delimiter "{{{W3M_CODE_BLOCK_END}}}")

  (defun w3m-filter-code-blocks (url)
    (w3m-tag-code-block-filter "code")
    (w3m-tag-code-block-filter "pre"))

  (defun w3m-tag-code-block-filter (tag)
    (goto-char (point-min))
    (let ((open-tag-re (format "<%s[ \t\r\f\n]*[^>]*>" tag))
	  (close-tag-re (format "</%s[ \t\r\f\n]*>" tag)))
      (while (re-search-forward open-tag-re nil t)
	(let ((start (match-beginning 0)))
	  (when (re-search-forward close-tag-re nil t)
	    (goto-char start)
	    (insert w3m-code-open-delimiter)
	    (goto-char (+ (string-width w3m-code-open-delimiter) (match-end 0)))
	    (insert w3m-code-close-delimiter))))))

  (defcustom w3m-code-block-face 'fixed-pitch
    "Face for <code> and <pre> blocks in w3m")

  (defun w3m-fontify-code-block ()
    (goto-char (point-min))
    (while (search-forward w3m-code-open-delimiter nil t)
      (let ((begin-block-start (match-beginning 0))
	    (begin-block-end (match-end 0))
	    (code-start (match-end 0)))
	(when (search-forward w3m-code-close-delimiter nil t)
	  (w3m-add-face-property code-start (match-beginning 0) w3m-code-block-face)
	  (delete-region (match-beginning 0) (match-end 0)))
	(delete-region begin-block-start begin-block-end)
	(goto-char (point-min)))))


  ;(add-hook 'w3m-fontify-before-hook 'w3m-fontify-code-block)

Add a w3m filter to handle the code block delimiters:

  ;; (with-eval-after-load 'w3m-filter
  ;;   (add-to-list 'w3m-filter-configuration
  ;;     '(t "Render code blocks with a different face" ".*" w3m-filter-code-blocks)))

Wakatime

Wakatime is a tool that tracks how much time you spend coding and various metrics about your development activity.

It needs a helper script to work properly.

  (use-package wakatime-mode
    :if (executable-find "wakatime")
    :init
    (setq wakatime-api-key (password-store-get "wakatime-api-key")
	  wakatime-cli-path (executable-find "wakatime"))
    :config
    (global-wakatime-mode)
    ;; global-wakatime-mode breaks recovering autosaves for some reason
    (advice-add 'recover-this-file :around
		(lambda (oldfn &rest args)
		  (let ((wakatime-was-enabled global-wakatime-mode))
		   (when wakatime-was-enabled
		     (global-wakatime-mode -1))
		   (apply oldfn args)
		   (when wakatime-was-enabled
		     (global-wakatime-mode))))))

Elfeed

Elfeed is a feed reader for Emacs.

  (defun elfeed-setup-hook ()
    (set (make-local-variable 'browse-url-browser-function)
         'w3m-browse-url)
    (set (make-local-variable 'jdormit/w3m-referer) "https://www.google.com")
    (general-def 'normal elfeed-search-mode-map "q" 'elfeed-search-quit-window)
    (general-def 'normal elfeed-search-mode-map "C-r" 'elfeed-search-update--force)
    (general-def 'normal elfeed-search-mode-map "R" 'elfeed-search-fetch)
    (general-def 'normal elfeed-search-mode-map "RET" 'elfeed-search-show-entry)
    (general-def 'normal elfeed-search-mode-map "s" 'elfeed-search-live-filter)
    (general-def 'normal elfeed-search-mode-map "S" 'elfeed-search-set-filter)
    (general-def 'normal elfeed-search-mode-map "B" 'elfeed-search-browse-url)
    (general-def 'normal elfeed-search-mode-map "y" 'elfeed-search-yank)
    (general-def 'normal elfeed-search-mode-map "u" 'elfeed-search-tag-all-unread)
    (general-def 'normal elfeed-search-mode-map "r" 'elfeed-search-untag-all-unread)
    (general-def 'normal elfeed-search-mode-map "n" 'next-line)
    (general-def 'normal elfeed-search-mode-map "p" 'previous-line)
    (general-def 'normal elfeed-search-mode-map "+" 'elfeed-search-tag-all)
    (general-def 'normal elfeed-search-mode-map "-" 'elfeed-search-untag-all)
    (general-def 'normal elfeed-show-mode-map "d" 'elfeed-show-save-enclosure)
    (general-def 'normal elfeed-show-mode-map "q" 'elfeed-kill-buffer)
    (general-def 'normal elfeed-show-mode-map "r" 'elfeed-show-refresh)
    (general-def 'normal elfeed-show-mode-map "n" 'elfeed-show-next)
    (general-def 'normal elfeed-show-mode-map "p" 'elfeed-show-prev)
    (general-def 'normal elfeed-show-mode-map "s" 'elfeed-show-new-live-search)
    (general-def 'normal elfeed-show-mode-map "b" 'elfeed-show-visit)
    (general-def 'normal elfeed-show-mode-map "y" 'elfeed-show-yank)
    (general-def 'normal elfeed-show-mode-map "u" (elfeed-expose #'elfeed-show-tag 'unread))
    (general-def 'normal elfeed-show-mode-map "+" 'elfeed-show-tag)
    (general-def 'normal elfeed-show-mode-map "-" 'elfeed-show-untag)
    (general-def 'normal elfeed-show-mode-map "SPC" 'scroll-up-command)
    (general-def 'normal elfeed-show-mode-map "DEL" 'scroll-down-command)
    (general-def 'normal elfeed-show-mode-map "\t" 'shr-next-link)
    (general-def 'normal elfeed-show-mode-map [tab] 'shr-next-link)
    (general-def 'normal elfeed-show-mode-map "\e\t" 'shr-previous-link)
    (general-def 'normal elfeed-show-mode-map [backtab] 'shr-previous-link)
    (general-def 'normal elfeed-show-mode-map [mouse-2] 'shr-browse-url)
    (general-def 'normal elfeed-show-mode-map "A" 'elfeed-show-add-enclosure-to-playlist)
    (general-def 'normal elfeed-show-mode-map "P" 'elfeed-show-play-enclosure))

  (use-package elfeed
    :commands elfeed
    :config
    (add-hook 'elfeed-show-mode-hook 'elfeed-setup-hook)
    (setq-default elfeed-search-filter "@6-months-ago +unread")
    (add-to-list 'evil-normal-state-modes 'elfeed-search-mode)
    (add-to-list 'evil-normal-state-modes 'elfeed-show-mode)
    (setq elfeed-feeds
          '(;; ("http://www.wsj.com/xml/rss/3_7085.xml" news)
            ;; ("https://www.wsj.com/xml/rss/3_7014.xml" news)
            ;; ("http://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml" news)
            ;; ("https://www.newyorker.com/feed/everything" news)
            ;; ("https://www.economist.com/sections/business-finance/rss.xml" news)
            ;; ("https://www.economist.com/sections/economics/rss.xml" news)
            ("https://taibbi.substack.com/feed" news)
            ("https://metaredux.com/feed.xml" clojure)
            ("https://emacsredux.com/atom.xml" emacs)
            ("https://sachachua.com/blog/category/emacs-news/feed" emacs)
            ("https://jeremydormitzer.com/blog/feed.xml" my-website))))

Keybinding for opening Elfeed:

  (leader-def-key "al" 'elfeed)

Undo Tree

  (use-package undo-tree
    :init
    (global-undo-tree-mode)
    (leader-def-key "bu" 'undo-tree-visualize)
    :general
    ('normal "u" #'undo-tree-undo)
    ('normal "C-r" #'undo-tree-redo)
    (undo-tree-visualizer-mode-map "SPC" leader-map))

Emojify

Because emojis make everything better.

  (use-package emojify
    :commands (emojify-mode
	       emojify-apropos-emoji
	       emojify-insert-emoji)
    :hook
    ((emacs-startup . emojify-mode))
    :init
    (leader-def-key "te" 'emojify-mode))

Calc

  (leader-def-key "ac" 'calc)

Deadgrep

A nice Emacs UI over ripgrep.

  (use-package deadgrep
    :commands deadgrep
    :general
    (deadgrep-mode-map "SPC" leader-map))

  (leader-def-key "fg" 'deadgrep)

RCIRC

IRC in Emacs, just in case anyone actually still uses it…

Channels:

  (with-eval-after-load 'rcirc
    (setq rcirc-server-alist
	  `(("znc.jeremydormitzer.com"
	     :port 3000
	     :nick "jdormit"
	     :user-name "jdormit"
	     :password ,(password-store-get "znc.jeremydormitzer.com")))))

Key bindings:

  (leader-def-key "ai" 'irc)

Use evil keybindings by default:

  (add-to-list 'evil-normal-state-modes 'rcirc-mode)

dumb-jump

Dumb-jump uses ripgrep and some algorithms based on the major-mode to implement jump-to-definition.

  (use-package dumb-jump
    :commands (dumb-jump-go dumb-jump-prompt)
    :config (dumb-jump-mode))

  (leader-def-key "cj" 'dumb-jump-go)
  (leader-def-key "cp" 'dumb-jump-go-prompt)

Dictionary

This package looks up word definitions online.

  (use-package define-word
    :commands (define-word define-word-at-point))

  (jdormit/define-prefix "r" "research")
  (leader-def-key "rd" 'define-word-at-point)
  (leader-def-key "rD" 'define-word)

Gnus

An ancient newsreader.

  (leader-def-key "ag" 'gnus)
  (general-def gnus-mode-map "SPC" leader-map)

It can read email:

  (setq gnus-select-method '(nnnil "")
	gnus-secondary-select-methods
	'((nnmaildir "jeremy@dormitzer.net"
		     (directory "~/.mail/jeremy-dormitzer-net"))
	  (nnmaildir "jeremy@getpterotype.com"
		     (directory "~/.mail/jeremy-getpterotype-com"))
	  (nnmaildir "jeremy.dormitzer@gmail.com"
		     (directory "~/.mail/jeremy-dormitzer-gmail-com"))
	  (nnmaildir "jdormitzer@hubspot.com"
		     (directory "~/.mail/jdormitzer-hubspot-com")))
	mm-text-html-renderer 'gnus-w3m)

Or Gnus can read RSS feeds directly:

  ;; (add-to-list 'gnus-secondary-select-methods
  ;;        '(nnrss "http://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml"))

Dired

Keybindings

  (general-def 'normal dired-mode-map "f" #'dired-create-empty-file)

Dired subtree

  (use-package dired-subtree
    :general
    (normal dired-mode-map "TAB" 'dired-subtree-toggle)
    :config
    (advice-add 'dired-subtree-toggle :after (lambda () (revert-buffer))))

Dired narrow

  (use-package dired-narrow
    :general
    (normal dired-mode-map "/" #'dired-narrow))

Dired sidebar

  (use-package dired-sidebar
    :commands (dired-sidebar-toggle-sidebar)
    :init
    (leader-def-key "d" #'dired-sidebar-toggle-sidebar)
    (add-hook 'dired-sidebar-mode-hook
              (lambda ()
                (unless (file-remote-p default-directory)
                  (auto-revert-mode))))
    :config
    (with-eval-after-load 'all-the-icons-dired
      ;; Display chevrons next to directories
      (defun all-the-icons-icon-for-dir-with-chevron (dir &optional chevron padding initial-padding &rest arg-overrides)
        "Format an icon for DIR with CHEVRON similar to tree based directories.

  If PADDING is provided, it will prepend and separate the chevron
  and directory with PADDING.

  Produces different symbols by inspecting DIR to distinguish
  symlinks and git repositories which do not depend on the
  directory contents"
        (let ((icon (apply 'all-the-icons-icon-for-dir dir arg-overrides))
              (chevron (if chevron (all-the-icons-octicon (format "chevron-%s" chevron) :height 0.8 :v-adjust -0.1) ""))
              (padding (or padding "\t")))
          (format "%s%s%s%s%s" initial-padding chevron padding icon padding)))

      (defun all-the-icons-dired-sidebar--refresh ()
        "Display the icons of files in a dired buffer."
        (all-the-icons-dired--remove-all-overlays)
        (save-excursion
          (goto-char (point-min))
          (while (not (eobp))
            (when (dired-move-to-filename nil)
              (let ((file (dired-get-filename 'relative 'noerror)))
                (when file
                  (let ((icon (if (file-directory-p file)
                                  (all-the-icons-icon-for-dir-with-chevron file
                                                                           (if (dired-subtree--is-expanded-p)
                                                                               "down"
                                                                             "right")
                                                                           "\t"
                                                                           ""
                                                                           :face 'all-the-icons-dired-dir-face
                                                                           :v-adjust all-the-icons-dired-v-adjust)
                                (all-the-icons-icon-for-file file :v-adjust all-the-icons-dired-v-adjust))))
                    (if (member file '("." ".."))
                        (all-the-icons-dired--add-overlay (point) " \t")
                      (all-the-icons-dired--add-overlay
                       (point)
                       (concat (when (not (file-directory-p file))
                                 " \t")
                               icon
                               "\t")))))))
            (forward-line 1))))

      (advice-add 'all-the-icons-dired--refresh :around
                  (lambda (oldfn &rest args)
                    (if (eq major-mode 'dired-sidebar-mode)
                        (apply #'all-the-icons-dired-sidebar--refresh args)
                      (apply oldfn args)))))

    (add-to-list 'dired-sidebar-special-refresh-commands 'dired-kill-subdir)
    :custom
    (dired-sidebar-should-follow-file t)
    (dired-sidebar-pop-to-sidebar-on-toggle-open nil)
    (dired-sidebar-no-delete-other-windows t))

All-the-icons

  (use-package all-the-icons)
  (use-package all-the-icons-dired
    :hook ((dired-mode . (lambda ()
                           (unless (eq major-mode 'dired-sidebar-mode)
                             (all-the-icons-dired-mode))))))

Variables

  (setq dired-dwim-target t)

Dired hydra

  (use-package dired
    :straight (:type built-in)
    :init
    (defhydra hydra-dired (:hint nil :color pink)
      "
  _+_ mkdir          _v_iew           _m_ark             _(_ details        _i_nsert-subdir    wdired
  _C_opy             _O_ view other   _U_nmark all       _)_ omit-mode      _$_ hide-subdir    C-x C-q : edit
  _D_elete           _o_pen other     _u_nmark           _l_ redisplay      _w_ kill-subdir    C-c C-c : commit
  _R_ename           _M_ chmod        _t_oggle           _g_ revert buf     _e_ ediff          C-c ESC : abort
  _Y_ rel symlink    _G_ chgrp        _E_xtension mark   _s_ort             _=_ pdiff
  _S_ymlink          ^ ^              _F_ind marked      _._ toggle hydra   \\ flyspell
  _r_sync            ^ ^              ^ ^                ^ ^                _?_ summary
  _z_ compress-file  _A_ find regexp
  _Z_ compress       _Q_ repl regexp

  T - tag prefix
  "
      ("\\" dired-do-ispell)
      ("(" dired-hide-details-mode)
      (")" dired-omit-mode)
      ("+" dired-create-directory)
      ("=" diredp-ediff)         ;; smart diff
      ("?" dired-summary)
      ("$" diredp-hide-subdir-nomove)
      ("A" dired-do-find-regexp)
      ("C" dired-do-copy)        ;; Copy all marked files
      ("D" dired-do-delete)
      ("E" dired-mark-extension)
      ("e" dired-ediff-files)
      ("F" dired-do-find-marked-files)
      ("G" dired-do-chgrp)
      ("g" revert-buffer)        ;; read all directories again (refresh)
      ("i" dired-maybe-insert-subdir)
      ("l" dired-do-redisplay)   ;; relist the marked or single directory
      ("M" dired-do-chmod)
      ("m" dired-mark)
      ("O" dired-display-file)
      ("o" dired-find-file-other-window)
      ("Q" dired-do-find-regexp-and-replace)
      ("R" dired-do-rename)
      ("r" dired-do-rsynch)
      ("S" dired-do-symlink)
      ("s" dired-sort-toggle-or-edit)
      ("t" dired-toggle-marks)
      ("U" dired-unmark-all-marks)
      ("u" dired-unmark)
      ("v" dired-view-file)      ;; q to exit, s to search, = gets line #
      ("w" dired-kill-subdir)
      ("Y" dired-do-relsymlink)
      ("z" diredp-compress-this-file)
      ("Z" dired-do-compress)
      ("q" nil)
      ("." nil :color blue))
    :general
    ((normal visual motion insert emacs) dired-mode-map "." 'hydra-dired/body)
    (dired-mode-map "SPC" leader-map))

Crontab

Magit ships with a cool utility called with-editor that lets you run a shell command using the current Emacs instance as $EDITOR. This means we can define a command to edit the crontab with the current Emacs instance:

  (defun edit-crontab ()
    (interactive)
    (with-editor-async-shell-command "crontab -e"))

Emacs Server

In case I need an emacsclient for some reason.

  (add-hook 'after-init-hook #'server-start)

YASnippet

YASnippet is Yet Another Snippet template system.

  (use-package yasnippet
    :config
    (unless (file-exists-p (expand-file-name "~/.emacs.d/snippets"))
      (mkdir (expand-file-name "~/.emacs.d/snippets") t))
    (setq yas-snippet-dirs
     `(,(concat (file-name-as-directory (get-dropbox-directory)) "yasnippet")
       ,(expand-file-name "~/.emacs.d/snippets")))
    (yas-global-mode))

  (use-package yasnippet-snippets
    :after (yasnippet)
    :config (yasnippet-snippets-initialize))

mpc

An Emacs interface to MPD, the Music Player Daemon

  (leader-def-key "ad" #'mpc)
  (with-eval-after-load 'evil
    (add-to-list 'evil-emacs-state-modes 'mpc-mode))
  (general-def mpc-mode-map "SPC" leader-map)
  (general-def mpc-mode-map "a" #'mpc-playlist-add)

wgrep

  (use-package wgrep
    :defer t)

Ivy

An alternative minibuffer completion framework:

  (use-package counsel
    :defer 0
    :config
    (ivy-mode 1)
    (counsel-mode 1)
    (setq ivy-height 20
          ivy-wrap t
          ivy-use-virtual-buffers nil
          ivy-count-format "%d/%d ")
    (with-eval-after-load 'projectile
      (setq projectile-completion-system 'ivy))
    (leader-def-key "SPC" #'counsel-M-x)
    (jdormit/define-prefix "i" "ivy")
    (jdormit/define-prefix "iU" "ui")
    (leader-def-key "ir" #'ivy-resume)
    (leader-def-key "ip" #'swiper-thing-at-point)
    (leader-def-key "iP" #'counsel-yank-pop)
    (leader-def-key "iu" #'counsel-unicode-char)
    (leader-def-key "iUt" #'counsel-load-theme)
    (leader-def-key "is" #'swiper)
    (leader-def-key "ia" #'swiper-all)
    (leader-def-key "ff" #'counsel-find-file)
    (leader-def-key "oc" #'counsel-org-capture)
    (leader-def-key "bb" #'counsel-ibuffer)

    (if (executable-find "rg")
        (leader-def-key "ig" #'counsel-rg)
      (leader-def-key "ig" #'counsel-grep))

    (defvar counsel-set-frame-font-history nil)
    (defun counsel-set-frame-font (font)
      (interactive (list (ivy-read "Font: " (delete-dups (font-family-list))
                                   :require-match t
                                   :history 'counsel-set-frame-font-history
                                   :caller 'counsel-set-frame-font)))
      (set-frame-font font))
    (leader-def-key "iUf" #'counsel-set-frame-font)

    (defun counsel-ibuffer-kill-buffer (x)
      (kill-buffer (cdr x)))
    (ivy-set-actions
     'counsel-ibuffer
     '(("k" counsel-ibuffer-kill-buffer "kill buffer")))

    ;; Function to open files without Ivy to avoid lag in really huge directories
    (defun find-file-default (filename &optional wildcards)
      (interactive
       (let ((completing-read-function 'completing-read-default))
         (find-file-read-args "Find file: "
                              (confirm-nonexistent-file-or-buffer))))
      (funcall-interactively 'find-file filename wildcards))

    :general
    ("C-c C-r" #'ivy-resume)
    ("M-x" #'counsel-M-x)
    ("C-x C-f" #'counsel-find-file)
    ("C-M-u" #'counsel-unicode-char)
    ("C-c P" #'counsel-yank-pop)
    ("C-s" #'swiper-isearch)
    (help-map "f" #'counsel-describe-function)
    (help-map "v" #'counsel-describe-variable)
    ((normal motion visual) "g/" #'swiper-thing-at-point))

  (use-package ivy-hydra
    :after counsel)

  (use-package counsel-projectile
    :after (counsel projectile)
    :commands (counsel-projectile
               counsel-projectile-switch-project
               counsel-projectile-find-file
               counsel-projectile-grep)
    :init
    (counsel-projectile-mode)
    (leader-def-key "pp" #'counsel-projectile-switch-project)
    (leader-def-key "pf" #'counsel-projectile)
    (if (executable-find "rg")
        (leader-def-key "pg" #'counsel-projectile-rg)
      (leader-def-key "pg" #'counsel-projectile-grep)))

  (use-package lsp-ivy
    :after (ivy lsp)
    :commands (lsp-ivy-workspace-symbol
               lsp-ivy-global-workspace-symbol))

  (leader-def-key "cs" #'lsp-ivy-workspace-symbol)
  (use-package ivy-xref
    :ensure t
    :straight (ivy-xref :fork (:host github :repo "jdormit/ivy-xref"))
    :init
    ;; xref initialization is different in Emacs 27 - there are two different
    ;; variables which can be set rather than just one
    (when (>= emacs-major-version 27)
      (setq xref-show-definitions-function #'ivy-xref-show-defs))
    ;; Necessary in Emacs <27. In Emacs 27 it will affect all xref-based
    ;; commands other than xref-find-definitions (e.g. project-find-regexp)
    ;; as well
    (setq xref-show-xrefs-function #'ivy-xref-show-xrefs))

graphviz

  (use-package graphviz-dot-mode
    :mode (("\\.dot\\'" . graphviz-dot))
    :init
    (with-eval-after-load 'org
     (add-to-list 'org-src-lang-modes '("dot" . graphviz-dot))))

Functions

A function that converts a lisp form into Graphviz format, e.g.:

  '(a ((label . "Node A"))
      (b ((label . "Node B"))
	 (d ((label . "Node D"))))
      (c ((label . "Node C"))
	 (e ((label . "Node E")))))

becomes:

  digraph {
	  a[label="Node A"];
	  b[label="Node B"];
	  c[label="Node C"];
	  d[label="Node D"];
	  e[label="Node E"];
	  a -> {b, c};
	  b -> d;
	  c -> e;
  }
  (defun graphviz-make-node-string (id attrs)
    "Makes a Graphviz Dot string representing a node with attributes"
    (if attrs
	(format
	 "%s[%s];"
	 id
	 (mapconcat 'identity
		    (mapcar
		     (lambda (attr)
		       (format "%s=\"%s\"" (car attr) (cdr attr)))
		     attrs)
		    ", "))
      (format "%s;" id)))

  (defun graphviz-make-edge-string (id children)
    "Makes a Graphviz Dot string representing the edges between id and children"
    (when children
      (format "%s -> {%s}"
	      id
	      (mapconcat
	       'identity
	       (mapcar
		(lambda (child)
		  (format "%s" (car child)))
		children)
	       "; "))))


  (defun graphviz-parse-graph (graph)
    "Parses a graph into nodes and edges represented in the dot language.
       Returns an alist ((nodes (<node strings>)) (edges (<edge strings>)))"
    (when graph
      (let* ((id (car graph))
	     (attrs (cadr graph))
	     (children (cddr graph))
	     (child-graphs (mapcar #'graphviz-parse-graph children)))
	`((nodes ,(cons (graphviz-make-node-string id attrs)
			(apply #'append
			       (mapcar (lambda (child)
					 (cadr (assoc 'nodes child)))
				       child-graphs))))
	  (edges ,(let ((edge-string (graphviz-make-edge-string id children))
			(child-edges (apply #'append
					    (mapcar (lambda (child)
						      (cadr (assoc 'edges child)))
						    child-graphs))))
		    (if edge-string
			(cons edge-string child-edges)
		      child-edges)))))))

  (defun graphviz-compile-graph (graph)
    "Transpiles a graph defined as a lisp form to the Graphviz Dot language"
    (let* ((graph (graphviz-parse-graph graph))
	   (nodes (cadr (assoc 'nodes graph)))
	   (edges (cadr (assoc 'edges graph))))
      (message "%s" graph)
      (format "digraph {\n        %s\n        %s\n}"
	      (mapconcat 'identity nodes "\n        ")
	      (mapconcat 'identity edges "\n        "))))

HideShow

hs-minor-mode enables comment and code-folding. It's useful almost everywhere, so just enable it:

  (add-hook 'prog-mode-hook (lambda () (hs-minor-mode 1)))

Slack

  (use-package slack
    :commands (slack-start)
    :init
    (setq slack-buffer-emojify t)
    (setq slack-prefer-current-team t)
    :config
    (slack-register-team
     :name "lolatravel"
     :default t
     ;;  :client-id "2400384563.846406584294"
     ;;   :client-secret "31d7bb557aebc773ef26fb53b0f3caf5"
     :token "xoxs-2400384563-531653313251-753670261511-dd9fd44b4755430caf78045af40361d9a2bf46d8a5392f09ea1b6f3c6db0b545"
     ;;   :token "xoxp-2400384563-531653313251-844227895840-6bd114a9d3d8642116f0a1d50675fa43"
     :subscribed-channels '(1-1-2020
			    backend
			    booking-team-backend
			    critical-bugs
			    dev
			    devops
			    frontend
			    search-and-book
			    smash
			    southwest
			    work
			    not-work)
     :full-and-display-names t)
    :general
    ('normal slack-info-mode-map ",u" #'slack-room-update-messages)
    ('normal slack-mode-map
	     ",c" 'slack-buffer-kill
	     ",ra" 'slack-message-add-reaction
	     ",rr" 'slack-message-remove-reaction
	     ",rs" 'slack-message-show-reaction-users
	     ",pl" 'slack-room-pins-list
	     ",pa" 'slack-message-pins-add
	     ",pr" 'slack-message-pins-remove
	     ",mm" 'slack-message-write-another-buffer
	     ",me" 'slack-message-edit
	     ",md" 'slack-message-delete
	     ",u" 'slack-room-update-messages
	     ",2" 'slack-message-embed-mention
	     ",3" 'slack-message-embed-channel
	     "\C-n" 'slack-buffer-goto-next-message
	     "\C-p" 'slack-buffer-goto-prev-message)
    ('normal slack-edit-message-mode-map
	     ",k" #'slack-message-cancel-edit
	     ",s" #'slack-message-send-from-buffer
	     ",2" #'slack-message-embed-mention
	     ",3" #'slack-message-embed-channel))
  (use-package alert
    :commands (alert)
    :init
    (setq alert-default-style (if (eq system-type 'darwin)
				  'osx-notifier
				'libnotify)))

Matrix

  (use-package matrix-client
    :commands matrix-client-connect
    :straight ((matrix-client :host github :repo "alphapapa/matrix-client.el"
			      :files (:defaults "logo.png" "matrix-client-standalone.el.sh"))))

EMMS

The Emacs Multi-Media System. For libtag to work, libtag must be installed on the system via the system package manager. Then Emms should be installed from source via:

  git clone git://git.sv.gnu.org/emms.git
  cd emms
  make emms-print-metadata
  make
  make install
  (add-to-list 'load-path "/usr/local/share/emacs/site-lisp/emms")
  (autoload 'emms-smart-browse "emms")
  (autoload 'emms-start "emms")
  (autoload 'emms-stop "emms")
  (autoload 'emms-pause "emms")
  (autoload 'emms-next "emms")
  (autoload 'emms-previous "emms")
  (autoload 'emms "emms")
  (autoload 'emms-play-directory-tree "emms")
  (autoload 'emms-all "emms-setup")
  (with-eval-after-load 'emms
    (emms-all)
    (emms-default-players)
    (require 'emms-info-libtag)
    (setq emms-info-functions
	  '(emms-info-libtag)
	  emms-source-file-default-directory
	  (concat (get-dropbox-directory) "/music"))
    (jdormit/define-prefix "ae" "emms")
    (with-eval-after-load 'evil
      (add-to-list 'evil-emacs-state-modes 'emms-browser-mode))
    (general-def emms-browser-mode-map "," leader-map)
    (general-def emms-playlist-mode-map "SPC" leader-map)
    (general-def emms-playlist-mode-map "," leader-map)
    (leader-def-key "aeb" 'emms-smart-browse)
    (leader-def-key "aes" 'emms-start)
    (leader-def-key "aeS" 'emms-stop)
    (leader-def-key "aeP" 'emms-pause)
    (leader-def-key "aen" 'emms-next)
    (leader-def-key "aep" 'emms-previous)
    (leader-def-key "aee" 'emms)
    (leader-def-key "aed" 'emms-play-directory-tree)
    (when (eq system-type 'darwin)
      (define-emms-simple-player afplay '(file)
	(regexp-opt '(".mp3" ".m4a" ".aac" ".m4p"))
	"afplay")
      (setq emms-player-list `(,emms-player-afplay))))

Direnv

Direnv automatically runs bash scripts when you enter certain directories. This sets it up to work with Emacs:

  (defun update-cider-env ()
    (direnv-update-directory-environment nrepl-project-dir))

  (use-package direnv
    :if (executable-find "direnv")
    :config
    (direnv-mode)
    (add-hook 'eshell-mode-hook #'direnv-update-directory-environment)
    (add-hook 'eshell-directory-change-hook
	      (lambda ()
		(unless (file-remote-p default-directory)
		  (direnv-update-directory-environment)))))

SQL

Emacs has excellent built-in SQL support.

  (leader-def-key "sP" #'sql-postgres)

GraphQL

GraphQL mode for editing GraphQL queries:

  (use-package graphql-mode
    :mode "\\.gql\\'"
    :commands (graphql-mode)
    :config
    (defvar graphql-env-alist '()
      "An alist defining available GraphQL servers

  The key is any symbol and the value is a cons pair of
  (graphql-url . graphql-auth-token)")
    (setq graphql-variables-file "~/variables.graphql")
    (defun graphql-set-env (env)
      (interactive
       (list
	(intern
	 (completing-read
	  "GraphQL env: "
	  (mapcar #'car graphql-env-alist)))))
      (let* ((gql-params (alist-get env graphql-env-alist))
	     (url (car gql-params))
	     (auth-token (cdr gql-params)))
	(setq graphql-url url
	      graphql-extra-headers `(("Authorization" . ,auth-token)))))
    (defun graphql (env)
      "Opens a graphql-mode buffer in the specified environment"
      (interactive
       (list
	(intern
	 (completing-read
	  "GraphQL env: "
	  (mapcar #'car graphql-env-alist)))))
      (graphql-set-env env)
      (let ((graphql-buf (get-buffer-create "*graphql-query*")))
	(set-buffer graphql-buf)
	(when (string= "" (buffer-substring (point-min) (point-max)))
	  (insert "{\n  \n}")
	  (goto-char 5))
	(graphql-mode)
	(switch-to-buffer graphql-buf)))
    (defun find-graphql-variables-file ()
      (interactive)
      (find-file graphql-variables-file))
    (jdormit/define-prefix "q" "graphql")
    (leader-def-key "qe" #'graphql-set-env)
    (leader-def-key "qf" #'find-graphql-variables-file)
    (leader-def-key "aq" #'graphql)
    (general-def graphql-mode-map "C-c C-e" #'graphql-set-env)
    (general-def graphql-mode-map "C-c C-v" #'find-graphql-variables-file))

GraphQL environments:

  (setq graphql-env-alist
    `((dev . ("https://api-dev.lola.co/api/graphql" .
	      ,(password-store-get "lola-graphql-dev-token")))
      (local . ("http://localhost:7200/api/graphql" .
		,(password-store-get "lola-graphql-local-token")))))

And ob-graphql for evaluating GraphQL source blocks in org-mode:

  (use-package ob-graphql
    :defer t)

Docker

Syntax highlighting for Dockerfiles:

  (use-package dockerfile-mode
    :mode ("\\Dockerfile\\'"))

Kubernetes

  (use-package kubernetes
    :ensure t
    :commands (kubernetes-overview)
    :init (leader-def-key "ak" #'kubernetes-overview)
    :config
    (add-to-list 'evil-emacs-state-modes 'kubernetes-overview-mode)
    (general-def kubernetes-overview-mode-map "SPC" leader-map))

AWS

S3

  (use-package s3ed
    :commands (s3ed-mode
               s3ed-find-file
               s3ed-save-file)
    :init
    (jdormit/define-prefix "fS" "s3")
    (leader-def-key "fSf" #'s3ed-find-file)
    (leader-def-key "fSs" #'s3ed-save-file)
    :config
    (defun browse-blob-in-emacs (url bufname)
      (let* ((token (password-store-get "blob-logs-token"))
             (buf (generate-new-buffer bufname))
             (blob (shell-command-to-string (concat "curl "
                                                    "-s "
                                                    "--cookie 'TOKEN=" token "' "
                                                    url)))
             (status
              (with-current-buffer buf
                (insert blob)
                (json-mode)
                (format-all-buffer))))
        (if (not (equal status "Formatting error"))
            (switch-to-buffer-other-window buf)
          (with-current-buffer buf
            (set-buffer-modified-p nil)
            (kill-buffer))
          (switch-to-buffer-other-window "*format-all-errors*"))))

    (defun s3ed-open-blob-log (arg)
      "Opens the blob log at point via the cloudfront proxy.

    If given prefix arg ARG, opens in browser, otherwise opens in Emacs."
      (interactive "P")
      (let ((fname-at-point (dired-file-name-at-point)))
        (if (string-match
             "\\(bloblogs.ops.lola.co[m]*\\)\/blobs\/\\(\\(production\\|dev\\|local\\|smoke\\|staging\\)\/[0-9]+/.*\\)"
             fname-at-point)
            (let* ((proxy-url (format "https://%s" (match-string 0 fname-at-point))))
              (if arg
                  (browse-url proxy-url)
                (browse-blob-in-emacs proxy-url fname-at-point)))
          (error "Not in a blob logs directory"))))
    :general
    ((normal) s3ed-mode-map "B" #'s3ed-open-blob-log))

Prodigy

Prodigy gives Emacs a nice way to run services (web servers, etc.).

  (use-package prodigy
    :commands (prodigy)
    :general
    ('normal 'prodigy-mode-map "SPC" leader-map)
    ('normal 'prodigy-view-mode-map "SPC" leader-map)
    :config
    (setq prodigy-completion-system 'default)
    (add-hook 'prodigy-view-mode-hook (lambda () (toggle-truncate-lines 1))))

  (leader-def-key "aP" #'prodigy)

Add the ability to associate a file with a service instead of a buffer:

  (with-eval-after-load 'prodigy
    (defun prodigy-service-file (service)
      "Return SERVICE file.

  If SERVICE file exists, use that. If not, find the first SERVICE
  tag that has a file and return that."
      (let ((file (prodigy-service-or-first-tag-with service :file)))
	(if (functionp file)
	    (prodigy-callback-with-plist file service)
	  file)))
    (defun prodigy-display-process-file-or-buffer ()
      (interactive)
      (when-let (service (prodigy-service-at-pos))
	(if-let (file (prodigy-service-file service))
	    (progn
	      (find-file-literally file)
	      (prodigy-view-mode)
	      (auto-revert-tail-mode)
	      (general-define-key
	       :states 'normal
	       :keymaps 'local
	       "Q" #'kill-this-buffer))
	  (prodigy-switch-to-process-buffer service))))
    (general-def 'normal prodigy-mode-map "`" #'prodigy-display-process-file-or-buffer))

And add the ability to inhibit all service output processing:

  (with-eval-after-load 'prodigy
    (defun prodigy-start-service (service &optional callback)
      "Start process associated with SERVICE unless already started.

  When CALLBACK function is specified, that is called when the
  process has been started.

  When the process is started, a timer starts and checks every
  second for `prodigy-start-tryouts' times if the process is live.
  If the process is not live after `prodigy-start-tryouts' seconds,
  the process is put in failed status."
      (declare (indent 1))
      (unless (prodigy-service-started-p service)
	(let* ((default-directory
		 (-if-let (cwd (prodigy-service-cwd service))
		     (f-full cwd)
		   default-directory))
	       (name (plist-get service :name))
	       (sudo (plist-get service :sudo))
	       (command (prodigy-service-command service))
	       (args (prodigy-service-args service))
	       (exec-path (append (prodigy-service-path service) exec-path))
	       (env (--map (s-join "=" it) (prodigy-service-env service)))
	       (process-environment (append env process-environment))
	       (process nil)
	       (create-process
		(lambda ()
		  (unless process
		    (setq process (apply (if sudo 'prodigy-start-sudo-process 'start-process)
					 (append (list name nil  command) args)))))))
	  (-when-let (init (prodigy-service-init service))
	    (funcall init))
	  (-when-let (init-async (prodigy-service-init-async service))
	    (let (callbacked)
	      (funcall
	       init-async
	       (lambda ()
		 (setq callbacked t)
		 (funcall create-process)))
	      (with-timeout
		  (prodigy-init-async-timeout
		   (error "Did not callback async callback within %s seconds"
			  prodigy-init-async-timeout))
		(while (not callbacked) (accept-process-output nil 0.005)))))
	  (funcall create-process)
	  (let ((tryout 0))
	    (prodigy-every 1
		(lambda (next)
		  (setq tryout (1+ tryout))
		  (if (process-live-p process)
		      (when callback (funcall callback))
		    (if (= tryout prodigy-start-tryouts)
			(prodigy-set-status service 'failed)
		      (funcall next))))))
	  (plist-put service :process process)
	  (when (not (plist-get service :inhibit-process-filter))
	    (set-process-filter
	     process
	     (lambda (_ output)
	       (run-hook-with-args 'prodigy-process-on-output-hook service output))))
	  (set-process-query-on-exit-flag process nil)))))

Lola

Some functions to make my day job easier.

Services (Prodigy)

  (defun call-with-venv (venv callback)
    (let ((venv-dir (cond
                     ((file-exists-p venv) venv)
                     ((file-exists-p
                       (substitute-in-file-name
                        (format "$WORKON_HOME/%s" venv)))
                      (substitute-in-file-name
                       (format "$WORKON_HOME/%s" venv)))
                     (t (error "virtual environment %s does not exist" venv)))))
      (call-with-env-from-file (format "%s/bin/activate" venv-dir) callback)))

  (defun kill-log-buffers ()
    (interactive)
    (kill-matching-buffers "\\.log$" nil t)
    (message "Killed log buffers"))

  (cl-defun python-service-setup (venv &optional env-file &key env-dir)
    (lambda (done)
      (call-with-venv
       venv
       (if env-file
           (lambda ()
             (call-with-env-from-file env-file done :dir env-dir))
         done))))

  (defun call-with-lola-env (callback)
    (let ((process-environment
           (cons (format "LOLA_ENV=%s"
                         (completing-read
                          "Environment: "
                          '("local" "development" "staging")))
                 process-environment)))
      (funcall callback)))

  (prodigy-define-tag
    :name 'lola)

  (prodigy-define-service
    :name "lola-server (gunicorn)"
    :tags '(lola backend)
    :command "bash"
    :args (lambda ()
            (list
             "-c"
             "gunicorn -c server/web/gunicorn.conf.py \
  -b 127.0.0.1:7200 bin.start_web:init_and_create_flask_app\\(\\) \
  >> ~/lola/logs/lola-server.log 2>&1"))
    :file "~/lola/logs/lola-server.log"
    :inhibit-process-filter t
    :cwd "~/lola/lola-server"
    :stop-signal 'int
    :truncate-output t
    :init-async (python-service-setup "lola-server"
                                      "~/lola/lola-server/.env"))

  (prodigy-define-service
    :name "lola-server celery worker"
    :tags '(lola backend)
    :command "python"
    :args '("bin/start_celery_worker.py" "-P" "gevent" "-n" "lola-server")
    :cwd "~/lola/lola-server"
    :stop-signal 'int
    :truncate-output t
    :init-async (python-service-setup "lola-server"
                                      "~/lola/lola-server/.env"))

  (prodigy-define-service
    :name "travel-service"
    :tags '(lola backend)
    :command "bash"
    :args (lambda ()
            (list
             "-c" "python bin/start_web.py >> ~/lola/logs/travel-svc.log 2>&1"))
    :cwd "~/lola/lola-travel-service"
    :file "~/lola/logs/travel-svc.log"
    :inhibit-process-filter t
    :stop-signal 'int
    :truncate-output t
    :init-async (python-service-setup "travel-service"
                                      "~/lola/lola-travel-service/.env"))

  (prodigy-define-service
    :name "travel-service celery worker"
    :tags '(lola backend)
    :command "bash"
    :args (lambda ()
            (list
             "-c"
             (concat "python "
                     "bin/start_celery_workers.py "
                     "-n " "travel-service "
                     "-Q "
                     "default,io_pool,cpu_pool,priority_io_pool,priority_cpu_pool "
                     ">> ~/lola/logs/travel-svc-celery.log 2>&1")))
    :file "~/lola/logs/travel-svc-celery.log"
    :inhibit-process-filter t
    :cwd "~/lola/lola-travel-service"
    :stop-signal 'int
    :truncate-output t
    :init-async (python-service-setup "travel-service"
                                      "~/lola/lola-travel-service/.env"))

  (prodigy-define-service
    :name "secrets"
    :tags '(lola backend)
    :command "python"
    :args '("bin/cmdline.py" "www")
    :cwd "~/lola/secrets"
    :truncate-output t
    :stop-signal 'int
    :init-async (python-service-setup "secrets"
                                      "~/lola/secrets/.env"))

  (prodigy-define-service
    :name "lola-desktop"
    :tags '(lola frontend)
    :command "npm"
    :args '("start")
    :cwd "~/lola/lola-desktop"
    :port 3001
    :env '(("PORT" "3001"))
    :stop-signal 'int
    :init-async #'call-with-lola-env)

  (prodigy-define-service
    :name "wallet"
    :tags '(lola frontend)
    :command "npm"
    :args '("start")
    :cwd "~/lola/wallet"
    :stop-signal 'int
    :env '(("PORT" "3000"))
    :init-async #'call-with-lola-env)

  (prodigy-define-service
    :name "agent-console"
    :tags '(lola frontend)
    :command "npm"
    :args '("start")
    :cwd "~/lola/agent-console"
    :stop-signal 'int
    :env '(("PORT" "3002"))
    :init-async (lambda (done)
                  (call-with-lola-env
                   (lambda ()
                     (nvm-use "v10.15.1" done)))))

  (prodigy-define-service
    :name "luigid"
    :command "luigid"
    :cwd "~/lola/data-pipeline"
    :port 8082
    :stop-signal 'int
    :init-async (python-service-setup "data-pipeline"
                                      "~/lola/data-pipeline/.env"))

  (prodigy-define-service
    :name "prometheus"
    :command "prometheus"
    :args '("--config.file=prometheus.yml")
    :port 9090
    :stop-signal 'int
    :cwd "~/prometheus")

  (prodigy-define-service
    :name "priceline-service"
    :tags '(lola backend)
    :command "~/lola/python_services/priceline/bin/start.sh"
    :args '("web")
    :cwd "~/lola/python_services"
    :stop-signal 'int
    :init-async (python-service-setup "~/lola/python_services/.venv"
                                      "~/lola/python_services/priceline/.env"
                                      :env-dir "~/lola/python_services"))

  (prodigy-define-service
    :name "priceline-cars-service"
    :tags '(lola backend)
    :command "bash"
    :args '("-c"
            "priceline_cars/bin/start.sh web >> ~/lola/logs/priceline-cars.log 2>&1")
    :cwd "~/lola/python_services"
    :inhibit-process-filter t
    :file "~/lola/logs/priceline-cars.log"
    :stop-signal 'int
    :init-async (python-service-setup "~/lola/python_services/.venv"
                                      "~/lola/python_services/priceline_cars/.env"
                                      :env-dir "~/lola/python_services"))

  (prodigy-define-service
    :name "threev-service"
    :tags '(lola backend)
    :command "~/lola/python_services/threev/bin/start.sh"
    :args '("web")
    :cwd "~/lola/python_services"
    :stop-signal 'int
    :init-async (python-service-setup "~/lola/python_services/.venv"
                                      "~/lola/python_services/threev/.env"
                                      :env-dir "~/lola/python_services"))

  (prodigy-define-service
    :name "ean-hotels-service"
    :tags '(lola backend)
    :command "bash"
    :args '("-c"
            "ean_hotels/bin/start.sh web >> ~/lola/logs/ean-hotels.log 2>&1")
    :cwd "~/lola/python_services"
    :inhibit-process-filter t
    :file "~/lola/logs/ean-hotels.log"
    :stop-signal 'kill
    :init-async (python-service-setup "~/lola/python_services/.venv"
                                      "~/lola/python_services/ean_hotels/.env"
                                      :env-dir "~/lola/python_services"))

  (prodigy-define-service
    :name "email-template-service"
    :tags '(lola backend)
    :command "npm"
    :args '("start")
    :cwd "~/lola/email-template-service"
    :env '(("PORT" "7300"))
    :stop-signal 'int
    :init-async (lambda (done)
                  (nvm-use "10.15.1" done)))

  (prodigy-define-service
    :name "mabl-link-agent"
    :command "link-agent"
    :args (lambda ()
            (list "-a" (password-store-get "mabl-link-agent")
                  "-n" "jdormit-macbook")))

  (prodigy-define-service
    :name "xray-daemon"
    :command "xray_mac"
    :args '("-o" "-n" "us-east-1"))

Services (eShell)

  (defun run-service-in-eshell (name dir cmd &optional setup)
    (if-let ((buf (get-buffer name)))
	(progn (when (eq major-mode 'eshell-mode)
		 (eshell-interrupt-process)
		 (while eshell-process-list))
	       (kill-buffer buf)))
    (let ((buf (get-buffer-create name)))
      (switch-to-buffer buf)
      (cd dir)
      (eshell-mode)
      (when setup (funcall setup))
      (insert cmd)
      (eshell-send-input)))

  (defun release-manager ()
    (interactive)
    (run-service-in-eshell "*release-manager*"
			   "~/lola/release-manager"
			   "pipenv run python release-manager"))

  (jdormit/define-prefix "L" "lola")
  (leader-def-key "Lr" #'release-manager)

Python stuff

Run pip install in the current project:

  (defun pip-install (reqs)
    (interactive
     (list
      (read-file-name "Requirements file: "
		      (or (projectile-project-root)
			  default-directory)
		      nil t "requirements"
		      (lambda (name)
			(string-match-p "requirements.*"
					name)))))
    (with-temp-buffer
      (cd (or (projectile-project-root)
	      default-directory))
      (compile (format "pip install -r %s" reqs))))

AWS-MFA

The aws-mfa command:

  (defun aws-mfa (mfa-token)
    (interactive "MMFA code: ")
    (let ((proc (start-process "aws-mfa"
			       "*aws-mfa*"
                               "aws-mfa"
			       "--force")))
      (set-process-sentinel
       proc
       (make-success-err-msg-sentinel "*aws-mfa*"
				      "AWS MFA succeeded"
				      "AWS MFA failed, check *aws-mfa* buffer for details"))
      (process-send-string proc (concat mfa-token "\n"))))

  (with-eval-after-load 'kubernetes
    (general-def kubernetes-overview-mode-map "m" #'aws-mfa))

1Password

  (defvar op-token nil
    "The 1Password session token")

  (defun 1pass-signin ()
    (interactive)
    (cl-letf* ((signin-address "team-lolatravel.1password.com")
	       (signin-email "jeremydormitzer@lola.com")
	       (secret-key (password-store-get "1pass-lola-secret-key"))
	       ((symbol-function 'op-signin)
		(make-shell-fn "pass" "team-lolatravel.1password.com" "|" "op" "signin" signin-address signin-email secret-key "--output=raw"))
	       (token (op-signin)))
      (if (string-match-p "ERROR" token)
	  (error "Unable to sign in to 1Password: %s" token)
	(setf op-token token)
	(message (format "Signed in to 1Password with session token %s" op-token))
	op-token)))

  (defun op-fn (&rest args)
    (lambda (&optional input)
      (cl-letf* (((symbol-function 'op-function)
		  (apply #'make-shell-fn "op" `(,@args ,(format "--session=%s" op-token))))
		 (output (op-function input)))
	(if (or (string-match-p "Authentication required" output)
		(string-match-p "You are not currently signed in" output))
	    (cl-letf* ((new-token (1pass-signin))
		       ((symbol-function 'op-function)
			(apply #'make-shell-fn "op" `(,@args ,(format "--session=%s" new-token)))))
	      (op-function input))
	  output))))

  (defun op-list-items ()
    (cl-flet ((op-list-items-fn (op-fn "list" "items")))
      (mapcar #'cdr
	      (mapcar (apply-partially #'assoc 'title)
		      (mapcar (apply-partially #'assoc 'overview)
			      (json-read-from-string (op-list-items-fn)))))))

  (defun op-get-item (item)
    (cl-flet ((op-get-item (op-fn "get" "item" item)))
      (json-read-from-string (op-get-item))))

  (defun op-get-item-field (item-json field-designation)
    (let* ((fields (assoc-recursive item-json 'details 'fields))
	   (pw-field (car (seq-filter
			   (lambda (field)
			     (string= field-designation (cdr (assoc 'designation field))))
			   fields))))
      (when pw-field (cdr (assoc 'value pw-field)))))

  (defun op-copy-password (item)
    (interactive
     (list
      (completing-read "1Password item: " (op-list-items))))
    (if-let ((password (op-get-item-field (op-get-item item) "password")))
	(with-temp-buffer
	  (insert password)
	  (copy-region-as-kill (point-min) (point-max))
	  (message "Copied password for \"%s\" to kill ring." item))
      ;; TODO if no password found, prompt for alternate field in record to return
      (error "No password found in 1Password for \"%s\"." item)))

  (leader-def-key "ao" #'op-copy-password)

Resetting DNSResponder

  (defun reset-dnsresponsder ()
    (interactive)
    (sudo-shell-command "killall -HUP mDNSResponder"))

Devpi

  (defvar devpi-indices nil
    "Login profiles for devpi. An alist
     where the keys are index names and
     the values are alists with the keys
     devpi-index-url, devpi-index-username,
     and devpi-index-password-fn.")

  (setq devpi-indices
	`((pip .
	       ((devpi-index-url . "https://pip.aws.lolatravel.com/pip/dev")
		(devpi-index-username . "pip")
		(devpi-index-password-fn . ,(lambda ()
					      (op-get-item-field
					       (op-get-item "PIP Devpi Login")
					       "password")))))
	  (jdormit .
		   ((devpi-index-url . "https://pip.aws.lolatravel.com/jdormit/dev")
		    (devpi-index-username . "jdormit")
		    (devpi-index-password-fn . ,(lambda ()
						  (password-store-get "devpi-jdormit")))))))

  (defun devpi-use-index (index-name)
    (interactive
     (list
      (intern
       (completing-read
	"Devpi index to use: "
	(mapcar #'car devpi-indices)))))
    (cl-letf* ((index-alist (alist-get index-name devpi-indices))
	       (index-url (alist-get 'devpi-index-url index-alist))
	       (index-username (alist-get 'devpi-index-username index-alist))
	       ((symbol-function 'index-password-fn) (alist-get 'devpi-index-password-fn index-alist))
	       (index-password (index-password-fn))
	       ((symbol-function 'devpi-use) (make-process-fn "devpi" "use" index-url))
	       ((symbol-function 'devpi-login)
		(make-process-fn "devpi" "login" index-username "--password" index-password))
	       (login-result (devpi-login))
	       (use-result (devpi-use)))
      (save-match-data
	(if (and (string-match "credentials valid" login-result)
		 (string-match "current devpi index" use-result))
	    (message "Switched to Devpi index %s" index-name)
	  (with-current-buffer (get-buffer-create "*devpi*")
	    (erase-buffer)
	    (goto-char (point-min))
	    (insert login-result)
	    (insert use-result))
	  (message "Failed to switch to Devpi index %s - check the *devpi* buffer for details" index-name)))))

  (defun devpi-upload ()
    (interactive)
    (cl-letf (((symbol-function 'devpi-upload) (make-process-fn "devpi" "upload")))
      (message (devpi-upload))))

Release notes

  (defvar release-notes-repo (expand-file-name
			      "~/lola/release-manager/release_notes")
    "The directory containing the release notes repository.")

  (defun lola-release-notes (staged &optional repos)
    "Prints release notes for the latest release.
  If called with a prefix argument, prints release notes
  for the upcoming release instead."
    (interactive "P")
    (let* ((all-repos '("lola-server"
			"lola-travel-service"
			"lola-desktop"))
	   (repos
	    (or
	     repos
	     (if (y-or-n-p "Query all repos?")
		 all-repos
	       (completing-read-multiple "Repos: "
					 all-repos)))))
      (with-env-from-file (expand-file-name
			   (concat release-notes-repo "/.env"))
	(async-shell-command
	 (format
	  (concat release-notes-repo "/venv/bin/python "
		  release-notes-repo "/release_notes/query_release_notes.py "
		  (when staged "--staged ")
		  " %s")
	  (combine-and-quote-strings repos))
	 "*release-notes*"))))

StumpWM

A handy keybinding to connect to the StumpWM SBCL process via SLIME:

  (defun connect-stumpwm ()
    (interactive)
    (slime-connect "127.0.0.1" 4004))

Emacs Network Client

Emacs frontend for networkmanager.

  (use-package enwc
    :config
    (setq enwc-default-backend 'nm)
    (add-to-list 'evil-emacs-state-modes 'enwc-mode)
    (general-def enwc-mode-map "SPC" leader-map)
    :init
    (leader-def-key "an" #'enwc)
    :commands (enwc))

Deft

A fuzzy-finder for notes.

  (use-package deft
    :commands (deft)
    :init
    (setq deft-extensions '("org" "txt" "md" "markdown" "text")
	  deft-recursive t
	  deft-directory (concat
			  (file-name-as-directory (get-dropbox-directory))
			  "org"))
    ;; Still lots of notes in the old Deft directory
    (add-to-list 'org-agenda-files
		 (concat (file-name-as-directory (get-dropbox-directory))
			 "/org/deft"))
    (leader-def-key "D" #'deft)
    (leader-def-key "od" #'deft)
    :config
    (setq deft-use-filter-string-for-filename t
	  deft-file-naming-rules '((noslash . "-")
				   (nospace . "-")
				   (case-fn . downcase))
	  deft-auto-save-interval 0)
    (add-to-list 'evil-emacs-state-modes 'deft-mode))

Tell Deft to use the TITLE property for entry titles, if it exists:

  (with-eval-after-load 'deft
    (advice-add
     'deft-parse-title :around
     (lambda (old-fn file contents &rest args)
       (let ((title (org-get-title contents)))
	 (if title
	     title
	   (apply old-fn file contents args))))))

Pollen

  (use-package pollen-mode
    :mode ("\\.p\\'" "\\.pp\\'" "\\.pm\\'"))

Ngrok

  (defun ngrok (port &optional subdomain)
    (interactive "nPort: \nsSubdomain: ")
    (let ((buf (get-buffer-create
		(concat "*ngrok-"
			(number-to-string port)
			"*")))
	  (cmd (if (and subdomain (not (string-empty-p subdomain)))
		   (concat "ngrok http "
			   "--subdomain=" subdomain " "
			   (number-to-string port))
		 (concat "ngrok http " (number-to-string port)))))
      (async-shell-command cmd buf)))

Make

  (defun make ()
    (interactive)
    (let ((project-root (projectile-project-root)))
      (if project-root
	  (with-temp-buffer
	    (cd project-root)
	    (async-shell-command "make"))
	(error "Not in a project"))))

Redis

  (defun redis-cli (&optional host port)
    (interactive (list (read-string "host (default localhost): " nil nil "localhost")
		       (read-number "port: " 6379)))
    (let ((cli-path (executable-find "redis-cli"))
	  (host (or host "localhost"))
	  (port (or port 6379)))
      (if cli-path
	  (progn
	    (make-comint-in-buffer
	     "redis-cli"
	     nil
	     "redis-cli"
	     nil
	     "-h" host
	     "-p" (number-to-string port)
	     "--no-raw")
	    (switch-to-buffer "*redis-cli*"))
	(error "Can't find redis-cli"))))

Restclient

Explore APIs from within Emacs!

  (use-package restclient
    :commands (restclient-mode))

  (use-package company-restclient
    :after (restclient company)
    :config
    (add-to-list 'company-backends 'company-restclient))

  (use-package ob-restclient
    :defer t
    :hook (org-mode . (lambda () (require 'ob-restclient))))

IMenu

Get a nice IMenu sidebar:

  (use-package imenu-list
    :config
    (setq imenu-list-focus-after-activation t)
    :general
    (imenu-list-major-mode-map "SPC" leader-map)
    (imenu-list-major-mode-map "." #'imenu-list-display-entry))

  (defun open-imenu-list ()
    (interactive)
    (if (and (fboundp 'lsp-ui-imenu)
	     (boundp 'lsp-mode)
	     lsp-mode)
	(lsp-ui-imenu)
      (imenu-list-smart-toggle)))

  (leader-def-key "\\" #'open-imenu-list)
  (leader-def-key "m" #'imenu)

calfw-org

A fancy calendar view:

  (use-package calfw
    :commands (cfw:open-calendar-buffer))

  (use-package calfw-org
    :config (require 'calfw)
    :commands (cfw:open-org-calendar))

  (leader-def-key "oC" #'cfw:open-org-calendar)

hackernews

  (use-package hackernews
    :commands (hackernews)
    :config
    (setq hackernews-internal-browser-function 'w3m-browse-url)
    :general
    ('normal hackernews-mode-map "SPC" leader-map))
  (leader-def-key "ah" 'hackernews)

counsel-spotify

Spotify in Emacs!

  (use-package counsel-spotify
    :commands (counsel-spotify-search-track
	       counsel-spotify-search-album
	       counsel-spotify-search-artist
	       counsel-spotify-seach-tracks-by-album
	       counsel-spotify-search-tracks-by-artist
	       counsel-spotify-play
	       counsel-spotify-toggle-play-pause
	       counsel-spotify-next
	       counsel-spotify-previous)
    :config
    (setq counsel-spotify-client-id "825add4224704126adf3912b847c86df"
	  counsel-spotify-client-secret "f71fb236c06b4af886658667056ef7bd"))

  (jdormit/define-prefix "iS" "counsel-spotify")
  (leader-def-key "iSt" #'counsel-spotify-search-track)
  (leader-def-key "iSa" #'counsel-spotify-search-album)
  (leader-def-key "iSA" #'counsel-spotify-search-artist)
  (leader-def-key "iS SPC" #'counsel-spotify-toggle-play-pause)
  (leader-def-key "iSn" #'counsel-spotify-next)
  (leader-def-key "iSp" #'counsel-spotify-previous)

ISpell

  (when (executable-find "hunspell")
    (setq ispell-program-name "hunspell"
	  ispell-really-hunspell t))

VTerm

A better terminal emulator for Emacs. Replaces ansi-term, not EShell.

  (defun eshell-exec-in-vterm (&rest args)
    (let* ((program (car args))
	   (buf (generate-new-buffer
		 (concat "*" (file-name-nondirectory program) "*"))))
      (with-current-buffer buf
	(vterm-mode)
	(vterm-send-string (concat (s-join " " args) "\n")))
      (switch-to-buffer buf)))

  (use-package vterm
    :if module-file-suffix
    :init
    (with-eval-after-load 'em-term
      (defun eshell-exec-visual (&rest args)
	(apply #'eshell-exec-in-vterm args)))
    :commands (vterm vterm-other-window vterm-mode))

  (defun run-vterm (&optional new-buffer)
    (interactive "P")
    (let ((buffer-name (when (not new-buffer) "vterm")))
      (if (and buffer-name (get-buffer buffer-name))
	  (switch-to-buffer buffer-name)
	(vterm buffer-name))))

  (defun open-vterm (&optional arg)
    (interactive "P")
    (if (and (fboundp 'projectile-project-root)
	     (projectile-project-root))
	(projectile-run-vterm arg)
      (run-vterm arg)))

  (leader-def-key "sv" 'open-vterm)

Hide mode line

Does what it says on the box.

  (use-package hide-mode-line
    :hook ((dired-sidebar-mode imenu-list-major-mode) . hide-mode-line-mode))

Dash

Dash is a code browser app for MacOS. dash-at-point is an Emacs interface to it.

  (use-package dash-at-point
    :if (file-exists-p "/Applications/Dash.app")
    :general
    ((normal visual motion insert emacs) "C-c d" 'dash-at-point)
    ((normal visual motion insert emacs) "C-c e" 'dash-at-point-with-docset))

Alfred

Add a function to power the alfred-org-capture Alfred workflow:

  (defun make-orgcapture-frame ()
    "Create a new frame and run org-capture."
    (interactive)
    (make-frame '((name . "Org Capture") (width . 100) (height . 24)
		  (top . 400) (left . 300)))

    (select-frame-by-name "Org Capture")
    (add-hook 'org-capture-after-finalize-hook #'delete-frame t t)
    (counsel-org-capture))

gist.el

Integrate with GitHub gists from Emacs!

  (use-package gist
    :defer t
    :config
    (add-hook 'gist-mode-hook
	      (lambda ()
		(evil-ex-define-local-cmd "w[rite]" 'gist-mode-save-buffer))))

ffap

Add the ability to jump to a particular line number in find-file-at-point. "Borrowed" from the EmacsWiki.

  (defvar ffap-file-at-point-line-number nil
    "Variable to hold line number from the last `ffap-file-at-point' call.")

  (defadvice ffap-file-at-point (after ffap-store-line-number activate)
    "Search `ffap-string-at-point' for a line number pattern and
  save it in `ffap-file-at-point-line-number' variable."
    (let* ((string (ffap-string-at-point)) ;; string/name definition copied from `ffap-string-at-point'
	   (name
	    (or (condition-case nil
		    (and (not (string-match "//" string)) ; foo.com://bar
			 (substitute-in-file-name string))
		  (error nil))
		string))
	   (line-number-string
	    (and (string-match ":[0-9]+" name)
		 (substring name (1+ (match-beginning 0)) (match-end 0))))
	   (line-number
	    (and line-number-string
		 (string-to-number line-number-string))))
      (if (and line-number (> line-number 0))
	  (setq ffap-file-at-point-line-number line-number)
	(setq ffap-file-at-point-line-number nil))))

  (defadvice find-file-at-point (after ffap-goto-line-number activate)
    "If `ffap-file-at-point-line-number' is non-nil goto this line."
    (when ffap-file-at-point-line-number
      (goto-line ffap-file-at-point-line-number)
      (setq ffap-file-at-point-line-number nil)))

Format-all-the-code

A package that bundles together common code beautifying tools for many languages (the actual formatting tools still need to be installed separately):

  (use-package format-all
    :commands (format-all-buffer)
    :init
    (leader-def-key "cf" 'format-all-buffer))

Compiling

Enable ANSI colors in compile buffers:

  (autoload 'ansi-color-apply-on-region "ansi-color")
  (defun colorize-compilation-buffer ()
    (ansi-color-apply-on-region compilation-filter-start (point-max)))
  (add-hook 'compilation-filter-hook #'colorize-compilation-buffer)

Set up some keybindings for Comint-mode compilation buffers:

  (general-def (normal visual motion) compilation-shell-minor-mode-map "q" #'quit-window)
  (general-def (normal visual motion) compilation-shell-minor-mode-map "gr" #'recompile)

Wallabag

My Wallabag client is still a WIP, but it is useful enough to pull in right now:

  (use-package wallabag
    :defer t
    :straight (:host github :repo "jdormit/emacs-wallabag-client")
    :custom
    (wallabag-base-url "https://wallabag.jeremydormitzer.com")
    (wallabag-client-id (password-store-get "wallabag-client-id"))
    (wallabag-client-secret (password-store-get "wallabag-client-secret"))
    (wallabag-username "jdormit")
    (wallabag-password (password-store-get "wallabag.jeremydormitzer.com")))

Clipmon

Syncs up the X Windows clipboard with the Emacs kill ring. Only necessary on Linux.

  (use-package clipmon
    :if (eq window-system 'x)
    :hook (after-init . clipmon-mode-start))

GUD

Emacs ships with a built-in debugger interface that I primarily use for PDB. The default keybindings are RSI-inducing, so here's a hydra to use instead:

  (defhydra hydra-gud (:hint nil)
    "
  _n_: step over         _b_: set breakpoint     _e_: execute statement  _l_: refresh buffer
  _s_: step into         _d_: remove breakpoint  _p_: print statement    _q_: quit hyra
  _>_: up stack frame    _f_: finish function    _w_: watch expression
  _<_: down stack frame  _c_: continue
  "
    (">" gud-up)
    ("<" gud-down)
    ("b" gud-break)
    ("d" gud-remove)
    ("e" gud-statement)
    ("f" gud-finish)
    ("l" gud-refresh)
    ("n" gud-next)
    ("p" gud-print)
    ("c" gud-cont :color blue)
    ("s" gud-step)
    ("w" gud-watch)
    ("q" nil))

  (leader-def-key "cg" #'hydra-gud/body)

realgud

Like GUD, but better!

  (use-package realgud
    :init
    (defun projectile-pdb ()
      (interactive)
      (with-projectile-root
       (funcall-interactively #'realgud:pdb))))

EDiff

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

Apache Drill

  (use-package sql-drill
    :straight (:host github :repo "jdormit/sql-drill.el")
    :commands (sql-drill)
    :init
    (when (file-exists-p "~/drill/apache-drill-1.17.0/bin/drill-embedded")
      (setq sql-drill-program
	    (expand-file-name "~/drill/apache-drill-1.17.0/bin/drill-embedded"))))

Helpful

A much-improved help buffer:

  (use-package helpful
    :defer t
    :init
    (setq counsel-describe-function-function #'helpful-callable
	  counsel-describe-variable-function #'helpful-variable)
    (add-hook 'emacs-lisp-mode-hook
	      (lambda ()
		(set (make-local-variable 'evil-lookup-func)
		     #'helpful-at-point)))
    :general
    (help-map "k" #'helpful-key))

Inform

Provides links to the help documentation for ELisp symbols from Info buffers:

  (use-package inform
    :straight (:host github :repo "dieter-wilhelm/inform")
    :defer 0
    :config
    ;; If Helpful is installed, link to Helpful buffers instead of vanilla help
    (with-eval-after-load 'helpful
     (define-button-type 'inform-function
       :supertype 'inform-xref
       'inform-function 'helpful-function
       'inform-echo (purecopy "mouse-2, RET: describe this function"))

     (define-button-type 'inform-variable
       :supertype 'inform-xref
       'inform-function 'helpful-variable
       'inform-echo (purecopy "mouse-2, RET: describe this variable"))

     (define-button-type 'inform-symbol
       :supertype 'inform-xref
       'inform-function #'helpful-symbol
       'inform-echo (purecopy "mouse-2, RET: describe this symbol"))))

Vuiet

A music browser and player:

  (use-package versuri
    :straight (versuri :host github :repo "mihaiolteanu/versuri")
    :defer t)

  (use-package vuiet
    :straight (vuiet :host github :repo "mihaiolteanu/vuiet")
    :commands (vuiet-track-artist
               vuiet-track-name
               vuiet-track-duration
               vuiet--playing-track)
    :defer t
    :config
    (setq vuiet-update-mode-line-automatically t
          vuiet-update-mode-line-interval 1)
    (defun vuiet-mode-setup ()
      (set (make-local-variable 'org-link-elisp-confirm-function)
           nil))
    (add-hook 'vuiet-mode-hook #'vuiet-mode-setup)
    :general
    ((normal motion visual) vuiet-mode-map "q" #'quit-window)
    ((normal motion visual) vuiet-mode-map "C-m" #'org-open-at-point))

And a handy hydra for it:

  (defun now-playing (length)
    (let* ((str (s-truncate length
                            (if-let ((track (vuiet--playing-track)))
                                (format "%s - %s"
                                        (vuiet-track-artist track)
                                        (vuiet-track-name track))
                              "----")))
           (remainder (- length (length str))))
      (if (> remainder 0)
          (let* ((left-pad (/ remainder 2))
                 (right-pad (- remainder left-pad)))
            (s-prepend (s-repeat left-pad " ")
                       (s-append (s-repeat right-pad " ") str)))
        str)))

  (defun track-pos ()
    (if-let ((track (vuiet--playing-track)))
        (format "[%s/%s]"
                (format-time-string "%M:%S" (condition-case nil
                                                (mpv-get-playback-position)
                                              (error 0)))
                (s-pad-left 5 "0" (vuiet-track-duration track)))
      "[00:00/00:00]"))

  (defvar hydra-vuiet-body-timer)
  (defun refresh-hydra-vuiet ()
    (interactive)
    (hydra-show-hint hydra-vuiet/hint 'refresh-hydra-vuiet))
  (defhydra hydra-vuiet (:hint nil
                         :color blue
                         :body-pre (setq hydra-vuiet-body-timer
                                               (run-at-time 1 1 #'refresh-hydra-vuiet))
                         :post (cancel-function-timers 'refresh-hydra-vuiet))
    "
                      ---            ^^                ^^            ^^                          ---
  +------------------/  /            ^^                ^^            ^^      +------------------/  /
  | -    -------    /  /             ^^                ^^            ^^      | -    -------    /  /
  |(-) .d########b. //)|+------------^^----------------^^------------^^-----+|(-) .d########b. //)| Play [_t_]ag(s)
  |  .d############//  ||+-----------^^----------------^^------------^^----+||  .d############//  | Play [_a_]rtist
  | .d######''####//b. |||%s(now-playing 43)^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^||| .d######''####//b. | Play a[_l_]bum
  | 9######(  )#-//##P ||+-----------^^--+-------------^^+-----------^^----+|| 9######(  )#-//##P |
  | 'b######++#/-/##d' ||            ^^  |%s(track-pos)^^|           ^^     || 'b######++#/-/##d' | [_L_]ove current track
  |  '9############P'  ||            ^^  +-------------^^+           ^^     ||  '9############P'  | [_U_]nlove current track
  |  -'9a#######aP'    ||  +---------^^+---------------^^--+---------^^--+  ||  -'9a#######aP'    |
  | |-|  `''''''       ||  |  _s_top   |   _p_lay/pause    |   _n_ext    |  || |-|  `''''''       | [_I_]nfo menu
  |  ---..-----------  ||  +---------^^+---------------^^--+---------^^--+  ||  ---..-----------  | [_S_]imilar menu
  | |---||-----------| |+------------^^----------------^^------------^^-----+| |---||-----------| |
  |                    |             ^^                ^^            ^^      |                    | [_q_]uit
  +--------------------+             ^^                ^^            ^^      +--------------------+
  "
    ("s" vuiet-stop)
    ("p" vuiet-play-pause :color red)
    ("n" vuiet-next :color red)
    ("t" vuiet-play-tag-similar)
    ("a" vuiet-play-artist)
    ("l" vuiet-play-album)
    ("L" vuiet-love-track)
    ("U" vuiet-unlove-track)
    ("I" hydra-vuiet-info/body)
    ("S" hydra-vuiet-similar/body)
    ("q" nil))
  (general-def "C-c v" #'hydra-vuiet/body)

  (defvar hydra-vuiet-info-body-timer)
  (defun refresh-hydra-vuiet-info ()
    (interactive)
    (hydra-show-hint hydra-vuiet-info/hint 'refresh-hydra-vuiet-info))
  (defhydra hydra-vuiet-info (:hint nil
                              :color blue
                              :body-pre (setq hydra-vuiet-info-body-timer
                                              (run-at-time 1 1 #'refresh-hydra-vuiet-info))
                              :post (cancel-function-timers 'refresh-hydra-vuiet-info))
    "
                      ---            ^^                ^^            ^^                          ---
  +------------------/  /            ^^                ^^            ^^      +------------------/  /
  | -    -------    /  /             ^^                ^^            ^^      | -    -------    /  /
  |(-) .d########b. //)|+------------^^----------------^^------------^^-----+|(-) .d########b. //)| Info for [_t_]ag
  |  .d############//  ||+-----------^^----------------^^------------^^----+||  .d############//  | Info for [_a_]rtist
  | .d######''####//b. |||%s(now-playing 43)^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^||| .d######''####//b. | Info for a[_l_]bum
  | 9######(  )#-//##P ||+-----------^^--+-------------^^+-----------^^----+|| 9######(  )#-//##P | Info for [_c_]urrent artist
  | 'b######++#/-/##d' ||            ^^  |%s(track-pos)^^|           ^^     || 'b######++#/-/##d' | L[_y_]rics for current track
  |  '9############P'  ||            ^^  +-------------^^+           ^^     ||  '9############P'  | [_L_]oved tracks
  |  -'9a#######aP'    ||  +---------^^+---------------^^--+---------^^--+  ||  -'9a#######aP'    |
  | |-|  `''''''       ||  |  _s_top   |   _p_lay/pause    |   _n_ext    |  || |-|  `''''''       | [_P_]layer menu
  |  ---..-----------  ||  +---------^^+---------------^^--+---------^^--+  ||  ---..-----------  | [_S_]imilar menu
  | |---||-----------| |+------------^^----------------^^------------^^-----+| |---||-----------| |
  |                    |             ^^                ^^            ^^      |                    | [_q_]uit
  +--------------------+             ^^                ^^            ^^      +--------------------+
  "
    ("s" vuiet-stop)
    ("p" vuiet-play-pause :color red)
    ("n" vuiet-next :color red)
    ("t" vuiet-tag-info)
    ("a" vuiet-artist-info-search)
    ("l" vuiet-album-info-search)
    ("c" vuiet-playing-artist-info)
    ("y" vuiet-playing-track-lyrics)
    ("L" vuiet-loved-tracks-info)
    ("P" hydra-vuiet/body)
    ("S" hydra-vuiet-similar/body)
    ("q" nil))

  (defvar hydra-vuiet-similar-body-timer)
  (defun refresh-hydra-vuiet-similar ()
    (interactive)
    (hydra-show-hint hydra-vuiet-similar/hint 'refresh-hydra-vuiet-similar))
  (defhydra hydra-vuiet-similar (:hint nil
                                 :color blue
                                 :body-pre (setq hydra-vuiet-similar-body-timer
                                                 (run-at-time 1 1 #'refresh-hydra-vuiet-similar))
                                 :post (cancel-function-timers 'refresh-hydra-vuiet-similar))
    "
                      ---            ^^                ^^            ^^                          ---
  +------------------/  /            ^^                ^^            ^^      +------------------/  /
  | -    -------    /  /             ^^                ^^            ^^      | -    -------    /  /
  |(-) .d########b. //)|+------------^^----------------^^------------^^-----+|(-) .d########b. //)| Play [_t_]ag similar
  |  .d############//  ||+-----------^^----------------^^------------^^----+||  .d############//  | Play [_a_]rtist similar
  | .d######''####//b. |||%s(now-playing 43)^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^||| .d######''####//b. | Play [_l_]oved tracks similar
  | 9######(  )#-//##P ||+-----------^^--+-------------^^+-----------^^----+|| 9######(  )#-//##P |
  | 'b######++#/-/##d' ||            ^^  |%s(track-pos)^^|           ^^     || 'b######++#/-/##d' | Play [_c_]urrent artist similar
  |  '9############P'  ||            ^^  +-------------^^+           ^^     ||  '9############P'  | Play current [_T_]ag similar
  |  -'9a#######aP'    ||  +---------^^+---------------^^--+---------^^--+  ||  -'9a#######aP'    |
  | |-|  `''''''       ||  |  _s_top   |   _p_lay/pause    |   _n_ext    |  || |-|  `''''''       | [_P_]layer menu
  |  ---..-----------  ||  +---------^^+---------------^^--+---------^^--+  ||  ---..-----------  | [_I_]nfo menu
  | |---||-----------| |+------------^^----------------^^------------^^-----+| |---||-----------| |
  |                    |             ^^                ^^            ^^      |                    | [_q_]uit
  +--------------------+             ^^                ^^            ^^      +--------------------+
  "
    ("s" vuiet-stop)
    ("p" vuiet-play-pause :color red)
    ("n" vuiet-next :color red)
    ("t" vuiet-play-tag-similar)
    ("a" vuiet-play-artist-similar)
    ("l" vuiet-play-loved-tracks-similar)
    ("c" vuiet-play-playing-artist-similar)
    ("T" vuiet-play-playing-tag-similar)
    ("P" hydra-vuiet/body)
    ("I" hydra-vuiet-info/body)
    ("q" nil))

Jira

Jira in Emacs:

  (use-package org-jira
    :init
    (setq jiralib-url "https://lola.atlassian.net"
          org-jira-working-dir (concat (get-dropbox-directory) "/org/jira")
          org-jira-jira-status-to-org-keyword-alist '(("To Do" . "TODO")
                                                      ("Blocked" . "BLOCKED")
                                                      ("In Progress" . "IN PROGRESS")
                                                      ("Code Review" . "IN PROGRESS")
                                                      ("Awaiting Release" . "IN PROGRESS")
                                                      ("Done" . "DONE")
                                                      ("Won't Fix" . "CANCELLED")))
    (add-to-list 'org-agenda-files org-jira-working-dir)
    (add-to-list 'org-super-agenda-groups
                 `(:name "Jira" :file-path ,org-jira-working-dir)
                 t))

Twitter

Socially networking from Emacs:

  (use-package twittering-mode
    :commands (twit)
    :init
    (leader-def-key "at" #'twit)
    :config
    (setq twittering-use-master-password t)
    (defun twittering-browse-uri (uri)
      (interactive (list (or (get-text-property (point) 'uri)
                             (if (get-text-property (point) 'field)
                                 (let* ((id (get-text-property (point) 'id))
                                        (status (twittering-find-status id)))
                                   (twittering-get-status-url-from-alist status))
                               nil))))
      (when uri (browse-url uri)))
    :general
    (twittering-mode-map "SPC" leader-map)
    (twittering-mode-map "W" #'twittering-browse-uri))

Structlog

  (use-package structlog-mode
    :straight (structlog-mode :repo "https://git.jeremydormitzer.com/jdormit/structlog-el.git")
    :config
    (setq structlog-db-username "jdormit"
          structlog-db-database "fluentd"))