John's Emacs Config

Table of Contents

Introduction

See init.el on how Org loads and interprets this file for initializing Emacs.

The latest raw version of this file can be found at https://github.com/john2x/emacs.d.

This file was last exported: 2019-10-04 16:36

System paths and files

Tell Emacs where to put packages installed from Melpa, where custom themes can be loaded and where customizations done with customize should be stored.

(push (expand-file-name "lib" "~/.emacs.d") load-path)
(push (expand-file-name "themes" "~/.emacs.d") custom-theme-load-path)

(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
  (when (file-exists-p custom-file)
  (load custom-file))

Constants

(defconst *is-a-mac* (eq system-type 'darwin))
(defconst *is-linux* (eq system-type 'gnu/linux))

Package Archives and Management

MELPA

Add MELPA and MELPA Stable as package archives.

(require 'package)
(add-to-list 'package-archives
  '("melpa" . "https://melpa.org/packages/") t)
(add-to-list 'package-archives
  '("melpa-stable" . "https://stable.melpa.org/packages/") t)

Pin these packages to use fetch from MELPA stable.

(setq package-pinned-packages '((cider . "melpa-stable")))

Initialize the package library.

(package-initialize)

Utility Functions and Macros

Define utility functions and macros used throughout this config file.

add-auto-mode

(defun add-auto-mode (mode &rest patterns)
  "Add entries to `auto-mode-alist' to use `MODE' for all given file `PATTERNS'."
  (dolist (pattern patterns)
    (add-to-list 'auto-mode-alist (cons pattern mode))))

Persistent scratch

Persist the *scratch* buffer every 5 minutes, so we don't lose any possibly important data if/when Emacs crashes.1

(defun save-persistent-scratch ()
  "Write the contents of *scratch* to the file name
`persistent-scratch-file-name'."
  (with-current-buffer (get-buffer-create "*scratch*")
    (write-region (point-min) (point-max) "~/.emacs-persistent-scratch")))

(defun load-persistent-scratch ()
  "Load the contents of `persistent-scratch-file-name' into the
  scratch buffer, clearing its contents first."
  (if (file-exists-p "~/.emacs-persistent-scratch")
      (with-current-buffer (get-buffer "*scratch*")
        (delete-region (point-min) (point-max))
        (insert-file-contents "~/.emacs-persistent-scratch"))))

(push #'load-persistent-scratch after-init-hook)
(push #'save-persistent-scratch kill-emacs-hook)

(if (not (boundp 'save-persistent-scratch-timer))
    (setq save-persistent-scratch-timer
          (run-with-idle-timer 300 t 'save-persistent-scratch)))

OS Specific

OS X

Conditionally set the following when on OS X (see Constants):

  1. Reveal file in current buffer in Finder.
  2. Use Command ⌘ for Meta and don't use Option ⌥.
  3. Fix mouse wheel/trackpad scrolling to be less "jerky".
  4. Bind ⌘+` to switch between frames.
(when *is-a-mac*
  ;; 1.
  ;; 2.
  (setq mac-command-modifier 'meta)
  (setq mac-option-modifier 'none)
  ;; 3.
  (setq mouse-wheel-scroll-amount '(1
                                    ((shift) . 5)
                                    ((control))))
  ;; 4.
  (global-set-key "\M-`" 'other-frame)
  (global-set-key "\M-~" (lambda () (interactive) (other-frame -1))))

The following adds packages installed by Homebrew to our load-path.

(if *is-a-mac*
  (let ((default-directory "/Applications/Emacs.app/Contents/Resources/site-lisp/"))
    (normal-top-level-add-subdirs-to-load-path)))

Linux

The following adds packages installed by Pacman to our load-path.

(if *is-linux*
  (let ((default-directory "/usr/share/emacs/site-lisp/"))
    (normal-top-level-add-subdirs-to-load-path)))

Load keychain environment variables, so we don't have to keep on typing our SSH passphrase.

(when *is-linux*
  (keychain-refresh-environment))

Make the kill ring work with X selections.

(setq select-enable-clipboard t
      select-enable-primary t)

TODO Windows

Try using this config on a Windows (7+) VM.

Install Packages

Install all packages here using install-package.

(defvar my-packages
  '(;;;; Misc
    exec-path-from-shell
    undo-tree
    bind-key
    avy
    link-hint
    swiper
    keychain-environment

    ;;;; Mode-line
    diminish
    smart-mode-line
    rich-minority

    ;;;; UI
    indent-guide
    yascroll
    highlight-symbol
    smooth-scroll

    ;;;; ido, ~M-x~
    flx-ido
    ido-completing-read+
    smex
    idomenu
    ido-vertical-mode

    ;;;; Window and frame management
    buffer-move
    ;window-number
    fullframe
    perspective
    nameframe
    edwina

    ;;;; Interactive Search
    anzu

    ;;;; Completion
    company
    company-emoji
    company-lsp
    company-terraform

    ;;;; Language Server Protocol
    lsp-mode
    lsp-ui
    ; lsp-python

    ;;;; Linting
    flycheck

    ;;;; Dired
    ;; dired+

    ;;;; Ack & Ag
    ag

    ;;;; Git
    magit
    git-blamed
    gitignore-mode
    gitconfig-mode
    git-messenger
    git-gutter
    browse-at-remote

    ;;;; Projectile
    projectile
    flx
    project-explorer

    ;;;; frame-purpose
    frame-purpose
    nameframe
    nameframe-projectile
    nameframe-perspective

    ;;;; Evil (Vim)
    evil
    evil-anzu
    evil-surround
    evil-leader
    evil-matchit
    evil-nerd-commenter
    evil-search-highlight-persist
    evil-vimish-fold

    ;;;; Ledger
    ledger-mode
    flycheck-ledger

    ;;;; Language specific
    ;;;;;; Python
    pyvenv
    nose
    elpy
    ;; ein

    ;;;;;; YAML
    yaml-mode

    ;;;;;; HTML, CSS
    web-mode

    ;;;;;; Markdown
    markdown-mode

    ;;;;;; Javascript
    json-mode
    js2-mode

    ;;;;;; Lisp
    paredit
    rainbow-delimiters
    highlight-parentheses
    paren-face

    ;;;;;; Clojure
    cider

    ;;;;;; Misc
    haskell-mode
    ghc
    flycheck-haskell
    purescript-mode
    elm-mode
    mu4e-alert
    restclient
    company-restclient
    origami

    ;;;;;; Org
    htmlize
    org-journal
    ;; ob-ipython
    toc-org)
  "My packages!")

;; loop over my-packages and install them
(defun install-my-packages ()
  (interactive)
  (mapc 'package-install my-packages))

(install-my-packages)

Configure

Now that everything is installed and ready, we can begin configuring packages, modes, key bindings, etc.

Misc

For a majority of programming modes, we want to indent immediately after a newline.

(add-hook 'prog-mode-hook
          (lambda () (local-set-key (kbd "RET") 'newline-and-indent)))

For a majority of programming languages, an underscore is part of a word or symbol.

(modify-syntax-entry  ?_ "w" (standard-syntax-table))

Set some generic variables.

(setq-default
 tab-width 4
 make-backup-files nil
 indent-tabs-mode nil
 show-trailing-whitespace t
 visible-bell nil)

We don't want to have to type "yes" or "no" at prompts.

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

Remember where we were when we last visited a file.

(setq-default save-place t)
(setq save-place-file "~/.emacs.d/tmp/saved-places")

Automatically creating missing parent directories when visiting a new file.

(defun my-create-non-existent-directory ()
      (let ((parent-directory (file-name-directory buffer-file-name)))
        (when (and (not (file-exists-p parent-directory))
                   (y-or-n-p (format "Directory `%s' does not exist! Create it?" parent-directory)))
          (make-directory parent-directory t))))
(add-to-list 'find-file-not-found-functions #'my-create-non-existent-directory)

When visiting buffers with the same name, uniqify them instead of the default of appending a number.

(setq uniquify-buffer-name-style 'forward
      uniquify-separator " • "
      uniquify-after-kill-buffer-p t
  ;; don't uniquify internal buffers (those that start with '*')
      uniquify-ignore-buffers-re "^\\*")

Bind undo/redo to sane bindings.

(require 'undo-tree)
(global-set-key (kbd "M-z") 'undo)
(global-set-key (kbd "M-Z") 'undo-tree-redo)

Interactive functions to encode/decode region for URLs.

(defun url-encode-region (beg end)
  "URL encode the region between BEG and END."
  (interactive "r")
  (if (use-region-p)
      (let* ((selected-text (buffer-substring beg end))
             (encoded-text (url-hexify-string selected-text)))
        (kill-region beg end)
        (insert encoded-text))))

(defun url-decode-region (beg end)
  "URL decode the region between BEG and END."
  (interactive "r")
  (if (use-region-p)
      (let* ((selected-text (buffer-substring beg end))
             (decoded-text (url-unhex-string selected-text)))
        (kill-region beg end)
        (insert decoded-text))))

Shell

;; make these environment variables available in Emacs
(with-eval-after-load 'exec-path-from-shell
  (dolist (var '("SSH_AUTH_SOCK"
                 "SSH_AGENT_PID"
                 "GPG_AGENT_INFO"
                 "LANG"
                 "LC_CTYPE"
                 "LEDGER_FILE"
                 "WORKON_HOME"))
    (add-to-list 'exec-path-from-shell-variables var)))
(when (memq window-system '(mac ns))
  (exec-path-from-shell-initialize))

UI

Configure UI stuff like:

  • hide toolbars
  • hide GUI scrollbars, use in-buffer scrollbars instead with yascroll
  • show indentation guide (useful for Python and HTML)
(require 'yascroll)
(require 'indent-guide)

;; don't show toolbar
(tool-bar-mode -1)

;; don't show menubar
(menu-bar-mode -1)

;; highlight matching parentheses
(show-paren-mode 1)

;; show line numbers
(global-display-line-numbers-mode)
(setq display-line-numbers-width 4)  ;; workaround for annoying issue of shifting line number width

;; we use yascroll for the scrollbar instead
(scroll-bar-mode -1)
(global-yascroll-bar-mode 1)
(setq yascroll:delay-to-hide nil)

;; show column number in mode-line
(column-number-mode)

(setq inhibit-splash-screen nil)

(setq-default indicate-empty-lines t)

;; enable indent-guide for the following modes only
(setq indent-guide-recursive nil)
;; (add-hook 'python-mode-hook 'indent-guide-mode)
(add-hook 'web-mode-hook 'indent-guide-mode)

Enable highlight-symbol in select modes. Also patch how symbols are (not) highlighted when holding down movement keys.

(dolist (hook '(prog-mode-hook html-mode-hook))
  (add-hook hook 'highlight-symbol-mode)
  (add-hook hook 'highlight-symbol-nav-mode)
  (add-hook hook 'vimish-fold-mode))
  ;(add-hook hook 'hs-minor-mode))

;; http://emacs.stackexchange.com/questions/931
(defun highlight-symbol-mode-post-command ()
  "After a command, change the temporary highlighting.
Remove the temporary symbol highlighting and, unless a timeout is specified,
create the new one."
  (if (eq this-command 'highlight-symbol-jump)
      (when highlight-symbol-on-navigation-p
        (highlight-symbol-temp-highlight))
    (highlight-symbol-update-timer highlight-symbol-idle-delay)))

(defun highlight-symbol-update-timer (value)
  (when highlight-symbol-timer
    (cancel-timer highlight-symbol-timer))
  (setq highlight-symbol-timer
        (run-with-timer value nil 'highlight-symbol-temp-highlight)))

(setq highlight-symbol-idle-delay .1)

Font

(defvar PragmataPro-font '(:family "PragmataPro" :size 13))
(defvar Go-font '(:family "Go Mono" :size 12))
(defvar Terminus-font '(:family "Terminus" :size 14))

(set-frame-font (apply 'font-spec PragmataPro-font) nil t)

(when *is-a-mac*
  (set-fontset-font
     t 'symbol
     (font-spec :family "Apple Color Emoji") nil 'prepend))

Easily switch fonts.

(defun my-switch-font (font)
  (interactive "sSwitch font (1. PragmataPro 2. Go Mono 3. Terminus): ")
  (cond ((string= font "1") (set-frame-font (apply 'font-spec PragmataPro-font) nil t))
        ((string= font "2") (set-frame-font (apply 'font-spec Go-font) nil t))
        ((string= font "3") (set-frame-font (apply 'font-spec Terminus-font) nil t))
        (t (message "Invalid option. Please choose 1 - 3."))))

Theme

Theme of the month.

(load-theme 'plan9 t)

Mode line

Show which function we are currently in in the mode line.

(which-function-mode)

Hide some minor modes from the mode line.

(rich-minority-mode)

(setq rm-blacklist '(" hl-p" " hl-s" " $" " hs" " zf" " company"
                     " GG" " FlyC" " Undo-Tree" " FlyC-" " Isearch"
                     " Anaconda" " Anzu"))

Other

Enable pixelwise resizing of frames, so they can be properly aligned by our window manager.

(setq frame-resize-pixelwise t)

Ag

Highlight search results in the ag buffer.

(setq ag-highlight-search t)

ido, M-x

(ido-mode t)
(ido-everywhere t)
(flx-ido-mode t)

(setq ido-enable-flex-matching t
      ido-use-filename-at-point nil
      ido-auto-merge-work-directories-length 0
;; Allow the same buffer to be open in different frames
      ido-default-buffer-method 'selected-window)

Render ido candidates vertically.

(ido-vertical-mode t)
(setq ido-vertical-define-keys 'C-n-and-C-p-only
      ido-vertical-show-count t)

Ignore dired buffers when using ido-switch-buffer, as we're only interested in actual file buffers (and some internal buffers).

(defun ido-ignore-dired-buffers (name)
  "Ignore dired buffers"
      (with-current-buffer name
        (derived-mode-p 'dired-mode)))
(add-to-list 'ido-ignore-buffers 'ido-ignore-dired-buffers)

Use ido in all interactions with M-x (i.e. provides ido-completion when doing M-x ledger-report, etc.)

(ido-ubiquitous-mode t)

Override M-x to use smex. Smex basically sorts commands by most-recently used.

(global-set-key (kbd "M-x") 'smex)
(global-set-key (kbd "M-X") 'smex-major-mode-commands)

Swiper, Ivy

(setq ivy-use-virtual-buffers t)
(global-set-key "\C-s" 'swiper)
(global-set-key (kbd "C-c C-r") 'ivy-resume)
(global-set-key (kbd "<f6>") 'ivy-resume)

Emulate Evil's * command with Swiper.

(global-set-key (kbd "C-M-s")
                (lambda ()
                  (interactive)
                  (swiper (word-at-point))))

Window and frame management

Use M-g [h|j|k|l] to swap buffers between windows. Also allow using numbers to switch window focus.

(require 'buffer-move)
(require 'window-number)

(dolist (fn '(buf-move-up buf-move-down buf-move-left buf-move-right))
  (let ((file "buffer-move"))
    (autoload fn file "Swap buffers between windows" t)))
(global-set-key (kbd "M-g h")       'buf-move-left)
(global-set-key (kbd "M-g l")       'buf-move-right)
(global-set-key (kbd "M-g k")       'buf-move-up)
(global-set-key (kbd "M-g j")       'buf-move-down)

(window-number-meta-mode t)
(window-number-mode t)

;; need to refresh the mode line format for each new frame
(add-hook 'window-configuration-change-hook (lambda () (window-number--update-mode-line-format)))

Cycle through a window's buffer history using C-M-,~ (backward) and ~C-M-. (forward).

(global-set-key (kbd "C-M-,") 'switch-to-prev-buffer)
(global-set-key (kbd "C-M-.") 'switch-to-next-buffer)

Enable Edwina for dwm-like window management. Need to enable it at the end of init to avoid a bug where the init file stops loading when trying to enable edwina.

(add-hook 'after-init-hook #'edwina-mode)

Interactive searching

(global-anzu-mode t)

(global-set-key [remap query-replace-regexp] 'anzu-query-replace-regexp)
(global-set-key [remap query-replace] 'anzu-query-replace)

;; Activate occur easily inside isearch
(define-key isearch-mode-map (kbd "C-o") 'isearch-occur)

Completion

company

Enable company-mode globally.

(require 'company)
(add-hook 'after-init-hook #'global-company-mode)

Add miscellaneous backends.

(add-to-list 'company-backends 'company-restclient)

Flycheck

(setq flycheck-check-syntax-automatically '(save idle-change mode-enabled)
      flycheck-idle-change-delay 0.8)
(add-hook 'after-init-hook #'global-flycheck-mode)

Language Specific

Python

Use Django style docstring format when filling docstrings.

(setq python-fill-docstring-style 'django)
  • LSP

    Emacs 27 has JSON improvements which make LSP perform LSP better. Use lsp-mode only when we are using Emacs 27+.

    Enable LSP for Python.

    (when (eq emacs-major-version 27)
      (require 'lsp-mode)
      (add-hook 'python-mode-hook #'lsp)
      (setq lsp-enable-snippet nil))
    

    LSP requires a language server to be installed in the virtual environment. Calling this function will do that.

    (defun lsp-python-install-packages ()
      "Install required Python packages for lsp."
      (interactive)
      (if 'pyvenv-virtual-env
          (async-shell-command "pip install -U python-language-server")
        (message "No active virtualenv.")))
    
  • elpy

    Use elpy when we are using Emacs 26 or lower.

    (unless (eq emacs-major-version 27)
      (elpy-enable))
    

    elpy requires certain Python packages to be installed in the virtualenv. These packages may not be included in the requirements.txt file for some Python projects. This command would install these required packages.

    (defun elpy-install-packages ()
      "Install required Python packages for elpy."
      (interactive)
      (if pyvenv-virtual-env
          (async-shell-command "pip install jedi flake8 autopep8 yapf")
        (message "No active virtualenv.")))
    
  • Virtual Environments

    Use pyvenv to manage virtual environments in Emacs.

    Define a command that would prompt the user to choose or create a virtualenv for the current project, and set the dir-locals file for that project.

    (defun my-python-project-dwim-virtualenv ()
      (interactive)
      ;; check if .dir-locals.el file already exists and if project-venv-name is in it
      ;; prompt user to choose existing venv or create a new one
      ;; update .dir-locals.el file
      )
    

    When switching focus to another frame, re-activate the proper virtualenv for that frame's project.

    (add-hook 'focus-in-hook (lambda ()
                               (hack-local-variables)
                               (if (boundp 'project-venv-name)
                               (progn
                                 (message "Activating %s" project-venv-name)
                                 (pyvenv-workon project-venv-name))
                               (progn (message "Deactivating")
                                      (pyvenv-deactivate)))))
    

    Show active virtualenv in mode line.

    ; (setq-default mode-line-format (cons '(:exec (concat "venv:" venv-current-name)) mode-line-format))
    
  • EIN

    The Emacs IPython Notebook package.

    Interactive function to create a frame dedicated for EIN usage. Make sure to have a Jupyter server up and running first.

    ;;(defun my-ein-frame ()
    ;;  "Open a frame dedicated for EIN."
    ;;  (interactive)
    ;;  ;; TODO: check if jupyter server is up and running before launching the frame
    ;;  (nameframe-with-frame "EIN"
    ;;    (persp-switch "ein")
    ;;    (call-interactively 'ein:notebooklist-login)))
    

YAML

(add-auto-mode 'yaml-mode "\\.ya?ml\\'")

HTML/CSS (web-mode)

We use web-mode for working with templates and enable it for the following filetypes.

(add-to-list 'auto-mode-alist '("\\.jinja2?\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.html?\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.css?\\'" . web-mode))

(setq web-mode-markup-indent-offset 4
      web-mode-css-indent-offset 4
      web-mode-code-indent-offset 4
      web-mode-enable-auto-quoting nil
      web-mode-enable-block-face t
      web-mode-enable-current-element-highlight t)

Use the appropriate web-mode engine when visiting a particular filetype. At the moment we default to the django engine for .html files. If you are in a project that uses jinja2 for templates, and the file extensions are in .html (a safe bet), then you'll need to define a .dir-locals.el file for that project, telling it to use the appropriate engine.

(setq web-mode-engines-alist
      '(("jinja2"    . "\\.jinja2\\'")
        ("django"    . "\\.html\\'")))

Markdown

(add-to-list 'auto-mode-alist '("\\.\\(md\\|markdown\\)\\'" . markdown-mode))

Javascript

We use js2-mode instead of the built-in js-mode.

(add-to-list 'auto-mode-alist '("\\.js\\'" . js2-mode))

(setq js2-use-font-lock-faces t
      js2-mode-must-byte-compile nil
      js2-basic-offset 2
      js2-indent-on-enter-key t
      js2-auto-indent-p t
      js2-bounce-indent-p nil)

(with-eval-after-load 'js2-mode
  (js2-imenu-extras-setup)
  (toggle-truncate-lines))

Lisp

Use pp-eval-expression. The same as eval-expression, but pretty-prints output.

(global-set-key (kbd "M-:") 'pp-eval-expression)

Define a list of "lispy" modes, so we can activate/deactivate stuff for all of them in a loop.

(require 'derived)

;; elisp only
(defconst elispy-modes
  '(emacs-lisp-mode ielm-mode))
;; all lisps
(defconst lispy-modes
  (append elispy-modes
          '(lisp-mode inferior-lisp-mode lisp-interaction-mode
            clojure-mode))
  "All lispy major modes.")

(defun my-lisp-setup ()
  "Enable features useful in any Lisp mode."
  ;; (rainbow-delimiters-mode t)
  ;; (hl-sexp-mode)
  (enable-paredit-mode)
  (turn-on-eldoc-mode)
  (highlight-parentheses-mode))

(dolist (hook (mapcar #'derived-mode-hook-name lispy-modes))
  (add-hook hook 'my-lisp-setup))

Check parentheses on save.

(defun maybe-check-parens ()
  "Run `check-parens' if this is a lispy mode."
  (when (memq major-mode lispy-modes)
    (check-parens)))

(add-hook 'after-save-hook 'maybe-check-parens)

Dim parentheses for Lisps.

(global-paren-face-mode)

Clojure

Hide *nrepl-connection* and *nrepl-server* buffers.

(setq nrepl-hide-special-buffers t)

Set some variables in CIDER REPL and some hooks.

(setq cider-repl-use-clojure-font-lock t)
(add-hook 'cider-repl-mode-hook 'subword-mode)
(add-hook 'cider-repl-mode-hook 'paredit-mode)
(add-hook 'cider-repl-mode-hook
          (lambda () (setq show-trailing-whitespace nil)))

Use clojure-mode for Clojurescript.

(add-auto-mode 'clojure-mode "\\.cljs\\'")

Elm

(add-hook 'elm-mode-hook #'elm-oracle-setup-completion)

Haskell

(add-hook 'haskell-mode-hook 'haskell-indentation-mode)
(eval-after-load 'flycheck
  '(add-hook 'flycheck-mode-hook #'flycheck-haskell-setup))

Terraform

Use LSP for Terraform when using Emacs 27.

(when (eq emacs-major-version 27)
  (add-to-list 'lsp-language-id-configuration '(terraform-mode . "terraform"))

  (lsp-register-client
   (make-lsp-client :new-connection (lsp-stdio-connection '("terraform-lsp" "-enable-log-file"))
                    :major-modes '(terraform-mode)
                    :server-id 'terraform-ls))

  (add-hook 'terraform-mode-hook #'lsp))

Use company-terraform when using Emacs 26 or below.

(unless (eq emacs-major-version 27)
  (add-to-list 'company-backends 'company-terraform))

Code Folding (HideShow)

(setq origami-fold-replacement "...")

Show the contents of the first 40 characters of the folded text and the number of lines folded.

(setq hs-set-up-overlay
      (defun my-hs-overlay (ov)
        (when (eq 'code (overlay-get ov 'hs))
          (overlay-put ov 'display
                       (propertize
                        (format " ... %s <%d> ... "
                                (replace-regexp-in-string
                                 "\n" ""
                                 (replace-regexp-in-string
                                  "^[ \t]*" ""
                                  (replace-regexp-in-string
                                   "[ \t]*$" ""
                                   (buffer-substring (overlay-start ov)
                                                     (+ (overlay-start ov) 40)))))
                                (count-lines (overlay-start ov)
                                             (overlay-end ov)))
                        'face 'diff-removed)))))

Dired

Don't hide details in dired.

(setq diredp-hide-details-initially-flag nil)

Define some keybindings for dired for quick navigation.

(defun bind-dired-utils-keys ()
  (bind-keys :map dired-mode-map
           ("." . dired-up-directory)
           ("M-o" . dired-subtree-insert)
           ("M-c" . dired-subtree-remove)
           ("M-u" . dired-subtree-up)
           ("M-d" . dired-subtree-down)
           ("M-p" . dired-subtree-previous-sibling)
           ("M-n" . dired-subtree-next-sibling)
           ("M->" . dired-subtree-end)
           ("M-<" . dired-subtree-beginning)
           ("C-c d" . dired-filter-by-directory)
           ("C-c f" . dired-filter-by-file)))

Setup dired+.

(with-eval-after-load 'dired
  (require 'dired+)
  (require 'dired-subtree)
  (require 'dired-filter)
  (when (fboundp 'global-dired-hide-details-mode)
    (global-dired-hide-details-mode -1))
  (setq dired-recursive-deletes 'top)
  (bind-dired-utils-keys)
  (define-key dired-mode-map [mouse-2] 'dired-find-file))

Open dired for the current directory when pressing C-x C-d.

(global-set-key (kbd "C-x C-d") '(lambda () (interactive) (dired ".")))

Omit uninteresting files in dired.

(add-hook 'dired-mode-hook (lambda () (dired-omit-mode)))

Org

Tell Org where our orgfiles are.

(setq org-directory "~/orgfiles")

Set custom TODO keywords.

(setq org-todo-keywords
      '((sequence "TODO" "DOING" "WAITING" "LATER" "|" "DONE" "DELEGATED" "CANCELED")))

Default notes file for org-capture.

(setq org-default-notes-file (concat org-directory "/notes.org"))

Set custom org-capture templates.

(setq org-capture-templates
      '(("t" "Todo" entry (file+headline (concat org-directory "/todo.org") "Other")
         "* TODO %?\n  %i\n  %a")
        ("n" "Note" entry (file+datetree (concat org-directory "/notes.org"))
         "* %?\nEntered on %U\n  %i\n  %a")))

(global-set-key (kbd "C-c o c") 'org-capture)

Add custom org-agenda command. We'd like to see at a glance:

  • Our agenda for the week
  • What we are currently working on
  • List of remaining TODO items
(setq org-agenda-custom-commands
      '(("z" "Agenda and Tasks"
         ((agenda "")
          (todo "DOING")
          (todo "TODO")))))

Enable font-locking for org source blocks.

(setq org-src-fontify-natively t)

Don't evaluate source blocks when exporting.

(setq org-export-babel-evaluate nil)

Allow quotes to be verbatim2, 3.

(add-hook 'org-mode-hook
          (lambda ()
            (setcar (nthcdr 2 org-emphasis-regexp-components) " \t\n,'")
            (org-set-emph-re 'org-emphasis-regexp-components org-emphasis-regexp-components)
            (org-element--set-regexps)
            (custom-set-variables `(org-emphasis-alist ',org-emphasis-alist))))

Enable the toc-org package, for generating and inserting table of contents directly in the Org document itself (e.g. useful for Github README.org files)

(add-hook 'org-mode-hook 'toc-org-enable)

Publishing

Allow exporting and publishing to ODT.

(require 'ox-odt)

;;;###autoload
  (defun org-odt-publish-to-odt (plist filename pub-dir)
    "Publish an org file to ODT.

  FILENAME is the filename of the Org file to be published.  PLIST
  is the property list of the given project.  PUB-DIR is the publishing
  directory.

  Return output file name."
    (unless (or (not pub-dir) (file-exists-p pub-dir)) (make-directory pub-dir t))
    ;; Check if a buffer visiting FILENAME is already open.
    (let* ((org-inhibit-startup t)
           (visiting (find-buffer-visiting filename))
           (work-buffer (or visiting (find-file-noselect filename))))
      (unwind-protect
      (with-current-buffer work-buffer
        (let ((outfile (org-export-output-file-name ".odt" nil pub-dir)))
          (org-odt--export-wrap
           outfile
           (let* ((org-odt-embedded-images-count 0)
                  (org-odt-embedded-formulas-count 0)
                  (org-odt-object-counters nil)
                  (hfy-user-sheet-assoc nil))
             (let ((output (org-export-as 'odt nil nil nil
                                          (org-combine-plists
                                           plist
                                           `(:crossrefs
                                             ,(org-publish-cache-get-file-property
                                               (expand-file-name filename) :crossrefs nil t)
                                             :filter-final-output
                                             (org-publish--store-crossrefs
                                              org-publish-collect-index
                                              ,@(plist-get plist :filter-final-output))))))
                   (out-buf (progn (require 'nxml-mode)
                                   (let ((nxml-auto-insert-xml-declaration-flag nil))
                                     (find-file-noselect
                                      (concat org-odt-zip-dir "content.xml") t)))))
               (with-current-buffer out-buf (erase-buffer) (insert output))))))))
      (unless visiting (kill-buffer work-buffer))))

Fix the path to the soffice program on macOS.

(setq org-odt-convert-processes '(("LibreOffice"
                                   "/Applications/LibreOffice.app/Contents/MacOS/soffice --headless --convert-to %f%x --outdir %d %i")))

Configure publishing of our orgfiles.

(setq org-export-date-timestamp-format "%Y-%m-%d")

(defun my-website-sitemap-function (project &optional sitemap-filename)
  "Custom sitemap generator that inserts additional options."
  (let ((sitemap (org-publish-sitemap-default project sitemap-filename)))
    (concat sitemap
            "\n\n#+OPTIONS: html-preamble:nil"
            "\n#+SUBTITLE: a.k.a. john2x"
            "\n#+HTML_HEAD_EXTRA: <style>body { font-family: CMU Serif, serif; margin: auto; max-width:768px; };</style>"
            (format "\n#+DATE:%s" (format-time-string "%Y-%m-%d")))))

(defun my-website-html-postamble (options)
  (concat "<hr>"
          (if (and (plist-get options ':keywords) (not (string= (plist-get options ':keywords) "")))
              (format "<p>Keywords: %s</p>" (plist-get options ':keywords))
              "")
          (format "<p class=\"date\">Modified: %s</p>" (format-time-string "%Y-%m-%d %H:%M:%S %Z"))
          (format "<p>Copyright (c) %s %s</p>"
                  (format-time-string "%Y") ;; TODO: get from document options
                  (car (plist-get options ':author)))
          (format "<p>%s</p>" (plist-get options ':creator))))

(setq org-publish-project-alist
      `(("orgfiles"
         :base-directory "~/Dropbox/orgfiles"
         :publishing-directory "~/Dropbox/orgfiles/published"
         :publishing-function org-html-publish-to-html
         :section-numbers nil
         :table-of-contents nil
         :recursive t
         :auto-sitemap t
         :sitemap-filename "sitemap.org"
         :sitemap-title "orgfiles")
        ("website-images"
         :base-directory "~/projects/misc/john2x.gitlab.io/images/"
         :base-extension "png\\|jpg\\|ico\\|gif"
         :publishing-directory "~/projects/misc/john2x.gitlab.io/public/images/"
         :publishing-function org-publish-attachment)
        ("website-static"
         :base-directory "~/projects/misc/john2x.gitlab.io/static/"
         :base-extension "css\\|js"
         :publishing-directory "~/projects/misc/john2x.gitlab.io/public/static/"
         :publishing-function org-publish-attachment)
        ("website-others"
         :base-directory "~/projects/misc/john2x.gitlab.io/"
         :base-extension "txt\\|xml\\|pdf\\|html"
         :publishing-directory "~/projects/misc/john2x.gitlab.io/public/"
         :publishing-function org-publish-attachment)
        ("website-content"
         :base-directory "~/projects/misc/john2x.gitlab.io/"
         :publishing-directory "~/projects/misc/john2x.gitlab.io/public/"
         :recursive t
         :exclude "level-.*\\|.*\.draft\.org\\|README\.org"
         :publishing-function org-html-publish-to-html
         :auto-sitemap t
         :sitemap-title "John Louis Del Rosario"
         :sitemap-filename "index.org"
         :sitemap-sort-files anti-chronologically
         :sitemap-function my-website-sitemap-function
         :html-link-up "/"
         :html-link-home "/"
         :html-preamble "<p class=\"date\">Published: %d</p>"
         :html-postamble my-website-html-postamble)
        ("website" :components ("website-content" "website-images" "website-others" "website-static"))))

Journal

Experiment with org-journal for a personal diary of sorts.

(setq org-journal-dir (concat org-directory "/journal/"))

Org Babel

Load additional languages.

; (org-babel-do-load-languages
;  'org-babel-load-languages
;  '((ipython . t)))

Set additional templates.

(setq org-structure-template-alist (append org-structure-template-alist
                                           '(("sel" "#+BEGIN_SRC emacs-lisp?\n\n#+END_SRC")
                                             ("sip" "#+BEGIN_SRC ipython :session?\n\n#+END_SRC")
                                             ("ex" "#+BEGIN_EXAMPLE\n\n#+END_EXAMPLE"))))

Git

Show git status indicators in the fringe.

(global-git-gutter-mode 1)
(git-gutter:linum-setup)
(setq git-gutter:modified-sign "* "
      git-gutter:added-sign "+ "
      git-gutter:deleted-sign "- "
      git-gutter:lighter " GG")

(global-set-key (kbd "M-g M-p") 'git-gutter:previous-hunk)
(global-set-key (kbd "M-g M-n") 'git-gutter:next-hunk)

Package for yanking/killing links to Git repository files.

(require 'browse-at-remote)

Magit

;; skip warning introduced by 1.4.0
(setq magit-last-seen-setup-instructions "1.4.0")

(setq-default
 magit-save-some-buffers nil
 magit-process-popup-time 10
 magit-diff-refine-hunk t
 magit-restore-window-configuration t
 magit-completing-read-function 'magit-ido-completing-read
 magit-revert-buffers nil)

(global-set-key (kbd "C-c m m") 'magit-status)

Make the Magit buffer take the entire frame.

(with-eval-after-load 'magit
  (fullframe magit-status magit-mode-quit-window))

Projectile and frame management

Configure Projectile for project management and navigation.

(projectile-global-mode)
(diminish 'projectile-mode)

(setq projectile-switch-project-action 'projectile-dired
      projectile-completion-system 'ido
      projectile-enable-caching t)

(global-set-key (kbd "C-x p") 'projectile-find-file)

Configure perspective and nameframe to have dedicated frames for each Projectile project.

(persp-mode)
(nameframe-projectile-mode 1)
(nameframe-perspective-mode 1)

(global-set-key (kbd "M-P") 'nameframe-switch-frame)

Not Projectile, but still project management related.

(global-set-key (kbd "<f3>") 'project-explorer-toggle)

ERC

Set some default values. We don't want to auto-reconnect too much since it could flood the channel and get us temporarily banned.

(setq erc-nick "john2x"
      erc-nick-uniquifier "_"
      erc-server-auto-reconnect t
      erc-server-reconnect-timeout 15)

Change header-line face when disconnected.

(defface erc-header-line-disconnected
  '((t (:inherit magit-diff-removed)))
  "Face to use when ERC has been disconnected.")

(defun erc-update-header-line-show-disconnected ()
  "Use a different face in the header-line when disconnected."
  (erc-with-server-buffer
    (cond ((erc-server-process-alive) 'erc-header-line)
          (t 'erc-header-line-disconnected))))

(setq erc-header-line-face-method 'erc-update-header-line-show-disconnected)

Interactive function to create a frame dedicated to ERC and automatically connect to preset servers. (We don't join channels automatically as it could take too long.)

(defun my-erc-frame ()
  "Switch or create to a frame called 'ERC' and connect to IRC"
  (interactive)
  (nameframe-with-frame "ERC"
                        (erc-tls :server "znc.john2x.com" :port "5000" :nick "john2x")))

When reconnect attempts fail, have a convenient shortcut to reconnect manually.

(with-eval-after-load 'erc
  (define-key erc-mode-map (kbd "C-c C-r") (lambda () (interactive) (erc-server-reconnect))))

When reconnecting, don't bring any channels up into the current buffer.

(setq erc-join-buffer 'window-noselect)

When using a VPN, freenode.net (and probably other servers as well) requires us to authenticate with SASL. Unfortunately, SASL support isn't implemented yet in the default ERC package bundled with Emacs.

There's an erc-sasl library4 but it requires patching the erc-login function so it sends the appropriate CAP request for SASL. Until erc-sasl gets merged into the main ERC package, we'll have to patch it here.

(require 'erc-sasl)
(add-to-list 'erc-sasl-server-regexp-list "irc\\.freenode\\.net")

(defun erc-login ()
  "Perform user authentication at the IRC server. (PATCHED)"
  (erc-log (format "login: nick: %s, user: %s %s %s :%s"
           (erc-current-nick)
           (user-login-name)
           (or erc-system-name (system-name))
           erc-session-server
           erc-session-user-full-name))
  (if erc-session-password
      (erc-server-send (format "PASS %s" erc-session-password))
    (message "Logging in without password"))
  (when (and (featurep 'erc-sasl) (erc-sasl-use-sasl-p))
    (erc-server-send "CAP REQ :sasl"))
  (erc-server-send (format "NICK %s" (erc-current-nick)))
  (erc-server-send
   (format "USER %s %s %s :%s"
       ;; hacked - S.B.
       (if erc-anonymous-login erc-email-userid (user-login-name))
       "0" "*"
       erc-session-user-full-name))
  (erc-update-mode-line))

.ircauthinfo is where we store our NickServ passwords, so we don't have to type it in all the time (and it breaks erc-login prompt when SASL is required).

(add-to-list 'auth-sources "~/.emacs.d/.ircauthinfo")

Set the prompt to use the channel name.

(setq erc-prompt  (lambda () (concat (buffer-name) " > ")))

Add a /FLUSH command to flush the ERC buffer of contents.

(defun erc-cmd-FLUSH (&rest ignore)
  "Erase the current buffer."
  (let ((inhibit-read-only t))
    (buffer-disable-undo)
    (erase-buffer)
    (buffer-enable-undo)
    (message "Flushed contents of channel")
    t))

Set the fill prefix to a constant value, instead of basing it off the username.

(setq erc-fill-prefix "        ↳ ")

Channel tracking is for keeping track of activity in channels which are currently not visible on some frame/window. Ignore tracking the following types of messages.

(setq erc-track-exclude-types '("JOIN" "NICK" "PART" "QUIT"))

Disable line numbers for ERC buffers.

(add-hook 'erc-mode-hook (lambda () (display-line-numbers-mode -1)))

Send messages with C-RET instead of just RET to avoid accidentally pasting text into an ERC buffer and pressing Enter.

(with-eval-after-load 'erc
  (define-key erc-mode-map (kbd "<C-return>") 'erc-send-current-line)
  (define-key erc-mode-map (kbd "RET") '(lambda () (interactive) (message "Send with C-return"))))

Modules

Highlight nicknames so they're easier to spot.

(require 'erc-highlight-nicknames)
(add-to-list 'erc-modules 'highlight-nicknames)

Use the services module to automatically attempt to identify with NickServ when connection to a server.

(add-to-list 'erc-modules 'services)

Save logs when leaving a channel.

(add-to-list 'erc-modules 'log)
(setq erc-save-buffer-on-part t)
(setq erc-log-channels-directory "~/.erc/logs")

Render smiley icons, because why not :-)?

(add-to-list 'erc-modules 'smiley)

Finally, reload ERC's modules.

(erc-update-modules)

Ledger

(defconst *ledger-journal-path* "~/Dropbox/ledger/john.ledger")
(defconst *ledger-docs-dir* "~/Dropbox/ledger/")

(add-to-list 'auto-mode-alist '("\\.ledger$" . ledger-mode))

(add-hook 'ledger-mode-hook 'goto-address-prog-mode)

;; don't override the highlighting of each posted item
;; in a xact if it is cleared/pending
(setq ledger-fontify-xact-state-overrides nil)

;; (defun my-ledger-frame ()
;;   "Easy way to open my ledger journal"
;;   (interactive)
;;   (nameframe-with-frame "ledger"
;;     (persp-switch "ledger")
;;     (find-file *ledger-journal-path*)
;;     (split-window-right)
;;     (find-file-other-window (concat *ledger-docs-dir* "Accounts.ledger"))
;;     (split-window-below)
;;     (window-number-select 1)
;;     (ledger-report "bal" nil)
;;     (toggle-frame-maximized)))

(with-eval-after-load 'flycheck
  (require 'flycheck-ledger))

Evil

Evil is meant to be enabled globally.

(evil-mode 1)

But we only want Normal state for particular modes, and use Emacs state everywhere else.

So first, we set Emacs state as Evil's default state.

(setq-default evil-default-state 'emacs)

We then clear Evil's whitelists of modes that should start in a particular state, so they all start in Emacs state.

(setq-default evil-insert-state-modes '())

Then we specify which modes we want Normal state for.

(setq-default evil-normal-state-modes
  '(clojure-mode
    python-mode
    ruby-mode
    erlang-mode
    emacs-lisp-mode
    web-mode
    css-mode
    js2-mode
    js-mode
    json-mode
    html-mode
    ledger-mode
    yaml-mode
    elixir-mode
    org-mode
    sh-mode
    haskell-mode
    elm-mode
    purescript-mode
    markdown-mode
    terraform-mode))

Set the evil-leader.

(require 'evil-leader)
(evil-leader/set-leader ",")
(global-evil-leader-mode)
(evil-leader/set-key "a g" 'ag)

Enable Evil plugins.

(global-evil-surround-mode 1)
(global-evil-matchit-mode 1)
(global-evil-search-highlight-persist t)
(evilnc-default-hotkeys)
(with-eval-after-load 'evil
  (require 'evil-anzu)
  (require 'evil-vimish-fold))
(evil-vimish-fold-mode 1)

Use SPACE for scrolling.

(define-key evil-normal-state-map (kbd "SPC") 'evil-scroll-down)
(define-key evil-normal-state-map (kbd "S-SPC") 'evil-scroll-up)

Bind some keys on the leader.

(evil-leader/set-key "n" 'evil-search-highlight-persist-remove-all)
(evil-leader/set-key "w" 'evil-write)

(defun my-evil-reload-buffer ()
  (interactive)
  (evil-edit nil t))
(evil-leader/set-key "e" 'my-evil-reload-buffer)

By default, C-u is bound to Emacs' universal-argument function, a rather important function used by various commands. But in Vim, C-u is supposed to scroll up half a page, and that has been burned into muscle memory by now. As a compromise, we bind universal-argument to M-u (which previously performs upcase-word, something we rarely, if ever, use), and use Vim's version of C-u to scroll up half a page.

(global-set-key (kbd "M-u") 'universal-argument)
(define-key universal-argument-map (kbd "M-u") 'universal-argument-more)
(with-eval-after-load 'evil-maps
  (define-key evil-motion-state-map (kbd "C-u") 'evil-scroll-up))

Fix visual select bug on macOS.

(fset 'evil-visual-update-x-selection 'ignore)

evil-nerd-commenter defines a global key binding for C-c p, which we do not use. Remove this binding and rebind the C-c p p binding for projectile-swith-project.

(global-unset-key (kbd "C-c p"))
(global-set-key (kbd "C-c p p") 'projectile-switch-project)

Footnotes:

Author: John Louis Del Rosario

Created: 2019-10-04 Fri 16:36

Validate