Skip to content
This repository has been archived by the owner on Jul 5, 2020. It is now read-only.

Latest commit

 

History

History
1427 lines (1174 loc) · 41 KB

init.org

File metadata and controls

1427 lines (1174 loc) · 41 KB

aminb’s Literate Emacs Configuration

About

This org file is my literate configuration for GNU Emacs, and is tangled to init.el. Packages are installed and managed using Borg. Over the years, I’ve taken inspiration from configurations of many different people. Some of the configurations that I can remember off the top of my head are:

I’d like to have a fully reproducible Emacs setup (part of the reason why I store my configuration in this repository) but unfortunately out of the box, that’s not achievable with package.el, not currently anyway. So, I’ve opted to use Borg. For what it’s worth, I briefly experimented with straight.el, but found that it added about 2 seconds to my init time; which is unacceptable for me: I use Emacs as my window manager (via EXWM) and coming from bspwm, I’m too used to having fast startup times.

Installation

To use this config for your Emacs, first you need to clone this repo, then bootstrap Borg, tell Borg to retrieve package submodules, and byte-compiled the packages. Something along these lines should work:

git clone https://github.com/aminb/dotfiles ~/.emacs.d
cd ~/.emacs.d
make bootstrap-borg
make bootstrap
make build

Contents

Header

First line

;;; init.el --- Amin Bandali's Emacs config -*- lexical-binding: t ; eval: (view-mode 1)-*-

Enable view-mode, which both makes the file read-only (as a reminder that init.el is an auto-generated file, not supposed to be edited), and provides some convenient key bindings for browsing through the file.

License

;; Copyright (C) 2018  Amin Bandali <amin@aminb.org>

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

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

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

Commentary

;;; Commentary:

;; Emacs configuration of Amin Bandali, computer scientist and functional
;; programmer.

;; THIS FILE IS AUTO-GENERATED FROM `init.org'.

Naming conventions

The conventions below were inspired by Doom’s conventions, found here. Naturally, I use my initials, ab, instead of doom.

;; Naming conventions:
;;
;;   ab-...   public variables or non-interactive functions
;;   ab--...  private anything (non-interactive), not safe for direct use
;;   ab/...   an interactive function; safe for M-x or keybinding
;;   ab:...   an evil operator, motion, or command
;;   ab|...   a hook function
;;   ab*...   an advising function
;;   ab@...   a hydra command
;;   ...!     a macro

Initial setup

;;; Code:

Emacs initialization

I’d like to do a couple of measurements of Emacs’ startup time. First, let’s see how long Emacs takes to start up, before even loading init.el, i.e. user-init-file:

(defvar ab--before-user-init-time (current-time)
  "Value of `current-time' when Emacs begins loading `user-init-file'.")
(message "Loading Emacs...done (%.3fs)"
         (float-time (time-subtract ab--before-user-init-time
                                    before-init-time)))

Also, temporarily increase gc-cons-threshhold and gc-cons-percentage during startup to reduce garbage collection frequency. Clearing the file-name-handler-alist seems to help reduce startup time as well.

(defvar ab--gc-cons-threshold gc-cons-threshold)
(defvar ab--gc-cons-percentage gc-cons-percentage)
(defvar ab--file-name-handler-alist file-name-handler-alist)
(setq gc-cons-threshold (* 400 1024 1024)  ; 400 MiB
      gc-cons-percentage 0.6
      file-name-handler-alist nil
      ;; sidesteps a bug when profiling with esup
      esup-child-profile-require-level 0)

Of course, we’d like to set them back to their defaults once we’re done initializing.

(add-hook
 'after-init-hook
 (lambda ()
   (setq gc-cons-threshold ab--gc-cons-threshold
         gc-cons-percentage ab--gc-cons-percentage
         file-name-handler-alist ab--file-name-handler-alist)))

Increase the number of lines kept in message logs (the *Messages* buffer).

(setq message-log-max 20000)

Optionally, we could suppress some byte compiler warnings like below, but for now I’ve decided to keep them enabled. See documentation for byte-compile-warnings for more details.

;; (setq byte-compile-warnings
;;       '(not free-vars unresolved noruntime lexical make-local))

whoami

(setq user-full-name "Amin Bandali"
      user-mail-address "amin@aminb.org")

Package management

No package.el

I can do all my package management things with Borg, and don’t need Emacs’ built-in package.el. Emacs 27 lets us disable package.el in the early-init-file (see here).

(setq package-enable-at-startup nil)

But since Emacs 27 isn’t out yet (Emacs 26 is just around the corner right now), and even when released it’ll be long before most distros ship in their repos, I’ll still put the old workaround with the commented call to package-initialize here anyway.

(setq package-enable-at-startup nil)
;; (package-initialize)

Borg

Assimilate Emacs packages as Git submodules

Borg is at the heart of package management of my Emacs setup. In short, it creates a git submodule in lib/ for each package, which can then be managed with the help of Magit or other tools.

(setq user-init-file (or load-file-name buffer-file-name)
      user-emacs-directory (file-name-directory user-init-file))
(add-to-list 'load-path
             (expand-file-name "lib/borg" user-emacs-directory))
(require 'borg)
(borg-initialize)

use-package

A use-package declaration for simplifying your .emacs

use-package is an awesome utility for managing and configuring packages (in our case especially the latter) in a neatly organized way and without compromising on performance.

(require 'use-package)
(if nil  ; set to t when need to debug init
    (setq use-package-verbose t
          use-package-expand-minimally nil
          use-package-compute-statistics t
          debug-on-error t)
  (setq use-package-verbose nil
        use-package-expand-minimally t))

Epkg

Browse the Emacsmirror package database

Epkg provides access to a local copy of the Emacsmirror package database, low-level functions for querying the database, and a package.el-like user interface for browsing the available packages.

(use-package epkg
  :defer t)

No littering in ~/.emacs.d

Help keeping ~/.emacs.d clean

By default, even for Emacs’ built-in packages, the configuration files and persistent data are all over the place. Use no-littering to help contain the mess.

(use-package no-littering
  :demand t
  :config
  (savehist-mode 1)
  (add-to-list 'savehist-additional-variables 'kill-ring)
  (save-place-mode 1)
  (setq auto-save-file-name-transforms
        `((".*" ,(no-littering-expand-var-file-name "auto-save/") t))))

Custom file (custom.el)

I’m not planning on using the custom file much, but even so, I definitely don’t want it mixing with init.el. So, here; let’s give it it’s own file. While at it, treat themes as safe.

(use-package custom
  :no-require t
  :config
  (setq custom-file (no-littering-expand-etc-file-name "custom.el"))
  (when (file-exists-p custom-file)
    (load custom-file))
  (setf custom-safe-themes t))

Better $PATH handling

Let’s use exec-path-from-shell to make Emacs use the $PATH as set up in my shell.

(use-package exec-path-from-shell
  :defer 1
  :init
  (setq exec-path-from-shell-check-startup-files nil)
  :config
  (exec-path-from-shell-initialize)
  ;; while we're at it, let's fix access to our running ssh-agent
  (exec-path-from-shell-copy-env "SSH_AGENT_PID")
  (exec-path-from-shell-copy-env "SSH_AUTH_SOCK"))

Only one custom theme at a time

(defadvice load-theme (before clear-previous-themes activate)
  "Clear existing theme settings instead of layering them"
  (mapc #'disable-theme custom-enabled-themes))

Server

Start server if not already running. Alternatively, can be done by issuing emacs --daemon in the terminal, which can be automated with a systemd service or using brew services start emacs on macOS. I use Emacs as my window manager (via EXWM), so I always start Emacs on login; so starting the server from inside Emacs is good enough for me.

See Using Emacs as a Server.

(use-package server
  :config (or (server-running-p) (server-mode)))

Unicode support

Font stack with better unicode support, around Ubuntu Mono and Hack.

(dolist (ft (fontset-list))
  (set-fontset-font
   ft
   'unicode
   (font-spec :name "Ubuntu Mono"))
  (set-fontset-font
   ft
   'unicode
   (font-spec :name "DejaVu Sans Mono")
   nil
   'append)
  ;; (set-fontset-font
  ;;  ft
  ;;  'unicode
  ;;  (font-spec
  ;;   :name "Symbola monospacified for DejaVu Sans Mono")
  ;;  nil
  ;;  'append)
  ;; (set-fontset-font
  ;;  ft
  ;;  #x2115  ; ℕ
  ;;  (font-spec :name "DejaVu Sans Mono")
  ;;  nil
  ;;  'append)
  (set-fontset-font
   ft
   (cons  )
   (font-spec :name "DejaVu Sans Mono" :size 14)
   nil
   'prepend))

Libraries

(require 'cl-lib)
(require 'subr-x)

Useful utilities

(defun ab-enlist (exp)
  "Return EXP wrapped in a list, or as-is if already a list."
(if (listp exp) exp (list exp)))

; from https://github.com/hlissner/doom-emacs/commit/589108fdb270f24a98ba6209f6955fe41530b3ef
(defmacro after! (features &rest body)
  "A smart wrapper around `with-eval-after-load'. Supresses warnings during
compilation."
  (declare (indent defun) (debug t))
  (list (if (or (not (bound-and-true-p byte-compile-current-file))
                (dolist (next (ab-enlist features))
                  (if (symbolp next)
                      (require next nil :no-error)
                    (load next :no-message :no-error))))
            #'progn
          #'with-no-warnings)
        (cond ((symbolp features)
               `(eval-after-load ',features '(progn ,@body)))
              ((and (consp features)
                    (memq (car features) '(:or :any)))
               `(progn
                  ,@(cl-loop for next in (cdr features)
                             collect `(after! ,next ,@body))))
              ((and (consp features)
                    (memq (car features) '(:and :all)))
               (dolist (next (cdr features))
                 (setq body `(after! ,next ,@body)))
               body)
              ((listp features)
               `(after! (:all ,@features) ,@body)))))

Core

Defaults

Time and battery in mode-line

Enable displaying time and battery in the mode-line, since I’m not using the Xfce panel anymore. Also, I don’t need to see the load average on a regular basis, so disable that.

(use-package time
  :ensure nil
  :init
  (setq display-time-default-load-average nil)
  :config
  (display-time-mode))

(use-package battery
  :ensure nil
  :config
  (display-battery-mode))

Smaller fringe

Might want to set the fringe to a smaller value, especially if using EXWM. I’m fine with the default for now.

;; (fringe-mode '(3 . 1))
(fringe-mode nil)

Disable disabled commands

Emacs disables some commands by default that could persumably be confusing for novice users. Let’s disable that.

(setq disabled-command-function nil)

Kill-ring

Save what I copy into clipboard from other applications into Emacs’ kill-ring, which would allow me to still be able to easily access it in case I kill (cut or copy) something else inside Emacs before yanking (pasting) what I’d originally intended to.

(setq save-interprogram-paste-before-kill t)

Minibuffer

(setq enable-recursive-minibuffers t
      resize-mini-windows t)

Lazy-person-friendly yes/no prompts

Lazy people would prefer to type fewer keystrokes, especially for yes or no questions. I’m lazy.

(defalias 'yes-or-no-p #'y-or-n-p)

Startup screen and *scratch*

Firstly, let Emacs know that I’d like to have *scratch* as my startup buffer.

(setq initial-buffer-choice t)

Now let’s customize the *scratch* buffer a bit. First off, I don’t need the default hint.

(setq initial-scratch-message nil)

Also, let’s use Text mode as the major mode, in case I want to customize it (*scratch*’s default major mode, Fundamental mode, can’t really be customized).

(setq initial-major-mode 'text-mode)

Inhibit the buffer list when more than 2 files are loaded.

(setq inhibit-startup-buffer-menu t)

I don’t really need to see the startup screen or echo area message either.

(advice-add #'display-startup-echo-area-message :override #'ignore)
(setq inhibit-startup-screen t
      inhibit-startup-echo-area-message user-login-name)

More useful frame titles

Show either the file name or the buffer name (in case the buffer isn’t visiting a file). Borrowed from Emacs Prelude.

(setq frame-title-format
      '("" invocation-name " - "
        (:eval (if (buffer-file-name)
                   (abbreviate-file-name (buffer-file-name))
                 "%b"))))

Backups

Emacs’ default backup settings aren’t that great. Let’s use more sensible options. See documentation for the make-backup-file variable.

(setq backup-by-copying t
      version-control t)

Auto revert

Enable automatic reloading of changed buffers and files.

(global-auto-revert-mode 1)
(setq auto-revert-verbose nil
      global-auto-revert-non-file-buffers t)

Always use space for indentation

(setq-default
 indent-tabs-mode nil
 require-final-newline t
 tab-width 4)

Packages

The packages in this section are absolutely essential to my everyday workflow, and they play key roles in how I do my computing. They immensely enhance the Emacs experience for me; both using Emacs, and customizing it.

(use-package auto-compile
  :demand t
  :config
  (auto-compile-on-load-mode)
  (auto-compile-on-save-mode)
  (setq auto-compile-display-buffer               nil
        auto-compile-mode-line-counter            t
        auto-compile-source-recreate-deletes-dest t
        auto-compile-toggle-deletes-nonlib-dest   t
        auto-compile-update-autoloads             t)
  (add-hook 'auto-compile-inhibit-compile-hook
            'auto-compile-inhibit-compile-detached-git-head))

Roll your own modal mode

(use-package ryo-modal
  :commands ryo-modal-mode
  :bind ("M-m" . ryo-modal-mode)
  :after which-key
  :config
  (push '((nil . "ryo:.*:") . (nil . "")) which-key-replacement-alist)
  (ryo-modal-keys
   ("," ryo-modal-repeat)
   ("b" backward-char)
   ("n" next-line)
   ("p" previous-line)
   ("f" forward-char)
   ("/" undo)
   ("i" ryo-modal-mode)
   ("l" recenter-top-bottom)
   ("v" scroll-up-command)
   ("V" scroll-down-command)
   ("x" delete-forward-char)
   ("SPC" (("b" (("b" ibuffer-list-buffers)
                 ("k" kill-this-buffer)
                 ("o" other-window)
                 ("s" save-buffer)))
           ("B" (("A" borg-activate)
                 ("a" borg-assimilate)
                 ("b" borg-build)
                 ("c" borg-clone)
                 ("r" borg-remove)))
           ("h" (("c" describe-char)
                 ("f" describe-function)
                 ("F" describe-face)
                 ("i" info)
                 ("k" describe-key)
                 ("l" view-lossage)
                 ("v" describe-variable)))
           ("q" (("q" save-buffers-kill-terminal)))))
   ("d" (("w" kill-word)
         ("b" backward-kill-word)))
   ("c w" kill-word :exit t))

  (ryo-modal-keys
   ;; First argyment to ryo-modal-keys may be a list of keywords.
   ;; These keywords will be applied to all keybindings.
   (:norepeat t)
   ("0" "M-0")
   ("1" "M-1")
   ("2" "M-2")
   ("3" "M-3")
   ("4" "M-4")
   ("5" "M-5")
   ("6" "M-6")
   ("7" "M-7")
   ("8" "M-8")
   ("9" "M-9"))
  :hook ((text-mode . ryo-modal-mode)
         (prog-mode . ryo-modal-mode)))

Org mode is for keeping notes, maintaining TODO lists, planning projects, and authoring documents with a fast and effective plain-text system.

In short, my favourite way of life.

(use-package org
  :ryo ("SPC b t" org-babel-tangle)
  :config
  (setq org-src-tab-acts-natively t
        org-src-preserve-indentation nil
        org-edit-src-content-indentation 0
        org-html-divs '((preamble  "header" "preamble")
                        (content   "main"   "content")
                        (postamble "footer" "postamble"))
        org-html-doctype "html5"
        org-html-html5-fancy t
        org-html-postamble nil)
  :hook (org-mode . org-indent-mode))
(use-package htmlize
  :after org)
(use-package org-notmuch
  :after (:any org notmuch))

It’s Magit! A Git porcelain inside Emacs.

Not just how I do git, but the way to do git.

(use-package magit
  :ryo ("SPC" (("g s" magit-status)))
  :defer t
  :bind (("s-g"     . magit-status)
         ("C-x g"   . magit-status)
         ("C-x M-g" . magit-dispatch-popup))
  :config
  (magit-add-section-hook 'magit-status-sections-hook
                          'magit-insert-modules
                          'magit-insert-stashes
                          'append))

Ivy (and friends)

Ivy - a generic completion frontend for Emacs, Swiper - isearch with an overview, and more. Oh, man!

There’s no way I could top that, so I won’t attempt to.

Ivy

(use-package ivy
  :defer 1
  :bind
  (:map ivy-minibuffer-map
        ([escape] . keyboard-escape-quit)
        ;; ("C-j"    . ivy-next-line)
        ;; ("C-k"    . ivy-previous-line)
        ([S-up]   . ivy-previous-history-element)
        ([S-down] . ivy-next-history-element)
        ("DEL"    . ivy-backward-delete-char))
  :ryo ("SPC ," ivy-switch-buffer)
  :config
  (setq ivy-wrap t)
  (ivy-mode 1))

Swiper

(use-package swiper
  :ryo
  ("SPC /" swiper)
  ("s" swiper)
  :bind (([remap isearch-forward]  . swiper)
         ([remap isearch-backward] . swiper)))

Counsel

(use-package counsel
  :defer 1
  :ryo
  ("SPC" (("f r" counsel-recentf)
          ("SPC" counsel-M-x)
          ("."   counsel-find-file)))
  :bind (([remap execute-extended-command] . counsel-M-x)
         ([remap find-file] . counsel-find-file)
         ("s-r"     . counsel-recentf)
         :map minibuffer-local-map
         ("C-r" . counsel-minibuffer-history))
  :config
  (counsel-mode 1)
  (defalias 'locate #'counsel-locate))

Borg’s layer/essentials

TODO: break this giant source block down into individual org sections.

(use-package dash
  :config (dash-enable-font-lock))

(use-package diff-hl
  :config
  (setq diff-hl-draw-borders nil)
  (global-diff-hl-mode)
  (add-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh t))

(use-package dired
  :defer t
  :config (setq dired-listing-switches "-alh"))

(use-package eldoc
  :when (version< "25" emacs-version)
  :config (global-eldoc-mode))

(use-package help
  :defer t
  :config (temp-buffer-resize-mode))

(progn ;    `isearch'
  (setq isearch-allow-scroll t))

(use-package lisp-mode
  :config
  (add-hook 'emacs-lisp-mode-hook 'outline-minor-mode)
  (add-hook 'emacs-lisp-mode-hook 'reveal-mode)
  (defun indent-spaces-mode ()
    (setq indent-tabs-mode nil))
  (add-hook 'lisp-interaction-mode-hook #'indent-spaces-mode))

(use-package man
  :defer t
  :config (setq Man-width 80))

(use-package paren
  :config (show-paren-mode))

(use-package prog-mode
  :config (global-prettify-symbols-mode)
  (defun indicate-buffer-boundaries-left ()
    (setq indicate-buffer-boundaries 'left))
  (add-hook 'prog-mode-hook #'indicate-buffer-boundaries-left))

(use-package recentf
  :demand t
  :config (add-to-list 'recentf-exclude "^/\\(?:ssh\\|su\\|sudo\\)?:"))

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

(use-package saveplace
  :when (version< "25" emacs-version)
  :config (save-place-mode))

(use-package simple
  :config (column-number-mode))

(progn ;    `text-mode'
  (add-hook 'text-mode-hook #'indicate-buffer-boundaries-left))

(use-package tramp
  :defer t
  :config
  (add-to-list 'tramp-default-proxies-alist '(nil "\\`root\\'" "/ssh:%h:"))
  (add-to-list 'tramp-default-proxies-alist '("localhost" nil nil))
  (add-to-list 'tramp-default-proxies-alist
               (list (regexp-quote (system-name)) nil nil)))

(use-package undo-tree
  :ryo
  ("?" undo-tree-undo)
  ("_" undo-tree-redo)
  :bind (("C-?" . undo-tree-undo)
         ("M-_" . undo-tree-redo))
  :config
  (global-undo-tree-mode)
  (setq undo-tree-mode-lighter ""
        undo-tree-auto-save-history t))

Editing

Company

(use-package company
  :defer 5
  :bind
  (:map company-active-map
        ([tab] . company-complete-common-or-cycle))
  :custom
  (company-idle-delay 0.3)
  (company-minimum-prefix-length 1)
  (company-selection-wrap-around t)
  (company-dabbrev-char-regexp "\\sw\\|\\s_\\|[-_]")
  :config
  (global-company-mode t))

Syntax and spell checking

(use-package flycheck
  :hook (prog-mode . flycheck-mode)
  :config
  ;; Use the load-path from running Emacs when checking elisp files
  (setq flycheck-emacs-lisp-load-path 'inherit)

  ;; Only flycheck when I actually save the buffer
  (setq flycheck-check-syntax-automatically '(mode-enabled save)))

Programming modes

(use-package alloy-mode
  :config (setq alloy-basic-offset 2))
(use-package proof-site  ; Proof General
  :load-path "lib/proof-site/generic/")
(use-package lean-mode
  :bind (:map lean-mode-map
              ("S-SPC" . company-complete)))

Haskell

(use-package haskell-mode
  :config
  (setq haskell-indentation-layout-offset 4
        haskell-indentation-left-offset 4
        flycheck-checker 'haskell-hlint
        flycheck-disabled-checkers '(haskell-stack-ghc haskell-ghc)))
(use-package dante
  :after haskell-mode
  :commands dante-mode
  :hook (haskell-mode . dante-mode))

Emacs bindings for hlint’s refactor option. This requires the refact executable from apply-refact.

(use-package hlint-refactor
  :bind (:map hlint-refactor-mode-map
              ("C-c l b" . hlint-refactor-refactor-buffer)
              ("C-c l r" . hlint-refactor-refactor-at-point))
  :hook (haskell-mode . hlint-refactor-mode))
(use-package flycheck-haskell)
:header-args+: :tangle lisp/hs-lint.el :mkdirp yes

Currently using flycheck-haskell with the haskell-hlint checker instead.

;;; hs-lint.el --- minor mode for HLint code checking

;; Copyright 2009 (C) Alex Ott
;;
;; Author: Alex Ott <alexott@gmail.com>
;; Keywords: haskell, lint, HLint
;; Requirements:
;; Status: distributed under terms of GPL2 or above

;; Typical message from HLint looks like:
;;
;; /Users/ott/projects/lang-exp/haskell/test.hs:52:1: Eta reduce
;; Found:
;;   count1 p l = length (filter p l)
;; Why not:
;;   count1 p = length . filter p


(require 'compile)

(defgroup hs-lint nil
  "Run HLint as inferior of Emacs, parse error messages."
  :group 'tools
  :group 'haskell)

(defcustom hs-lint-command "hlint"
  "The default hs-lint command for \\[hlint]."
  :type 'string
  :group 'hs-lint)

(defcustom hs-lint-save-files t
  "Save modified files when run HLint or no (ask user)"
  :type 'boolean
  :group 'hs-lint)

(defcustom hs-lint-replace-with-suggestions nil
  "Replace user's code with suggested replacements"
  :type 'boolean
  :group 'hs-lint)

(defcustom hs-lint-replace-without-ask nil
  "Replace user's code with suggested replacements automatically"
  :type 'boolean
  :group 'hs-lint)

(defun hs-lint-process-setup ()
  "Setup compilation variables and buffer for `hlint'."
  (run-hooks 'hs-lint-setup-hook))

;; regex for replace suggestions
;;
;; ^\(.*?\):\([0-9]+\):\([0-9]+\): .*
;; Found:
;; \s +\(.*\)
;; Why not:
;; \s +\(.*\)

(defvar hs-lint-regex
  "^\\(.*?\\):\\([0-9]+\\):\\([0-9]+\\): .*[\n\C-m]Found:[\n\C-m]\\s +\\(.*\\)[\n\C-m]Why not:[\n\C-m]\\s +\\(.*\\)[\n\C-m]"
  "Regex for HLint messages")

(defun make-short-string (str maxlen)
  (if (< (length str) maxlen)
      str
    (concat (substring str 0 (- maxlen 3)) "...")))

(defun hs-lint-replace-suggestions ()
  "Perform actual replacement of suggestions"
  (goto-char (point-min))
  (while (re-search-forward hs-lint-regex nil t)
    (let* ((fname (match-string 1))
          (fline (string-to-number (match-string 2)))
          (old-code (match-string 4))
          (new-code (match-string 5))
          (msg (concat "Replace '" (make-short-string old-code 30)
                       "' with '" (make-short-string new-code 30) "'"))
          (bline 0)
          (eline 0)
          (spos 0)
          (new-old-code ""))
      (save-excursion
        (switch-to-buffer (get-file-buffer fname))
        (goto-char (point-min))
        (forward-line (1- fline))
        (beginning-of-line)
        (setf bline (point))
        (when (or hs-lint-replace-without-ask
                  (yes-or-no-p msg))
          (end-of-line)
          (setf eline (point))
          (beginning-of-line)
          (setf old-code (regexp-quote old-code))
          (while (string-match "\\\\ " old-code spos)
            (setf new-old-code (concat new-old-code
                                 (substring old-code spos (match-beginning 0))
                                 "\\ *"))
            (setf spos (match-end 0)))
          (setf new-old-code (concat new-old-code (substring old-code spos)))
          (remove-text-properties bline eline '(composition nil))
          (when (re-search-forward new-old-code eline t)
            (replace-match new-code nil t)))))))

(defun hs-lint-finish-hook (buf msg)
  "Function, that is executed at the end of HLint execution"
  (if hs-lint-replace-with-suggestions
      (hs-lint-replace-suggestions)
      (next-error 1 t)))

(define-compilation-mode hs-lint-mode "HLint"
  "Mode for check Haskell source code."
  (set (make-local-variable 'compilation-process-setup-function)
       'hs-lint-process-setup)
  (set (make-local-variable 'compilation-disable-input) t)
  (set (make-local-variable 'compilation-scroll-output) nil)
  (set (make-local-variable 'compilation-finish-functions)
       (list 'hs-lint-finish-hook))
  )

(defun hs-lint ()
  "Run HLint for current buffer with haskell source"
  (interactive)
  (save-some-buffers hs-lint-save-files)
  (compilation-start (concat hs-lint-command " \"" buffer-file-name "\"")
                     'hs-lint-mode))

(provide 'hs-lint)
;;; hs-lint.el ends here
(use-package hs-lint
  :load-path "lisp/"
  :bind (:map haskell-mode-map
              ("C-c l l" . hs-lint)))

Emacs Enhancements

Emacs package that displays available keybindings in popup

(use-package which-key
  :defer 1
  :config (which-key-mode))
(use-package doom-modeline
  :demand t
  :config (setq doom-modeline-height 32)
  :hook (after-init . doom-modeline-init))
(use-package tao-theme
  :demand t
  :config (load-theme 'tao-yang t))
(load-theme 'eink t)
(use-package crux
  :bind (("C-c d"    . crux-duplicate-current-line-or-region)
         ("C-c M-d"  . crux-duplicate-and-comment-current-line-or-region))
  :ryo
  ("o" crux-smart-open-line :exit t)
  ("O" crux-smart-open-line-above :exit t)
  ("SPC b K" crux-kill-other-buffers)
  ("d d" crux-kill-whole-line)
  ("c c" crux-kill-whole-line :then '(crux-smart-open-line-above) :exit t)
  ("SPC f" (("c" crux-copy-file-preserve-attributes)
            ("D" crux-delete-file-and-buffer)
            ("R" crux-rename-file-and-buffer))))
(use-package mwim
  :bind (("C-a"    . mwim-beginning-of-code-or-line)
         ("C-e"    . mwim-end-of-code-or-line)
         ("<home>" . mwim-beginning-of-line-or-code)
         ("<end>"  . mwim-end-of-line-or-code))
  :ryo
  ("a" mwim-beginning-of-code-or-line)
  ("e" mwim-end-of-code-or-line))
(use-package key-chord
  :demand t
  :config
  (key-chord-mode 1)
  (key-chord-define-global "jk" 'ryo-modal-mode)
  (setq key-chord-one-key-delay 0 ; i don't need one-key chords for now
        key-chord-two-keys-delay 0.005))

Email

See bug follow-up.

(defun ab/notmuch ()
  "Delete other windows, then launch `notmuch'."
  (interactive)
  (require 'notmuch)
  (delete-other-windows)
  (notmuch))

;; (map!
;;  :leader
;;  :desc "notmuch" :n "m" #'ab/notmuch
;;  (:desc "search" :prefix "/"
;;    :desc "notmuch" :n "m" #'counsel-notmuch))
(defvar ab-maildir "~/mail")

(use-package sendmail
  ;; :ensure nil
  :config
  (setq sendmail-program "/usr/bin/msmtp"
        mail-specify-envelope-from t
        mail-envelope-from 'header))

(use-package message
  ;; :ensure nil
  :config
  (setq message-kill-buffer-on-exit t
        message-send-mail-function 'message-send-mail-with-sendmail
        message-sendmail-envelope-from 'header
        message-directory "drafts"
        message-user-fqdn "aminb.org")
  (add-hook 'message-mode-hook
            (lambda () (setq fill-column 65
                        message-fill-column 65)))
  (add-hook 'message-mode-hook
            #'flyspell-mode)
  ;; (add-hook 'notmuch-message-mode-hook #'+doom-modeline|set-special-modeline)
  ;; TODO: is there a way to only run this when replying and not composing?
  ;; (add-hook 'notmuch-message-mode-hook
  ;;           (lambda () (progn
  ;;                   (newline)
  ;;                   (newline)
  ;;                   (forward-line -1)
  ;;                   (forward-line -1))))
  ;; (add-hook 'message-setup-hook
  ;;           #'mml-secure-message-sign-pgpmime)
  )

(after! mml-sec
  (setq mml-secure-openpgp-encrypt-to-self t
        mml-secure-openpgp-sign-with-sender t))

(use-package notmuch
  :ryo ("SPC m" ab/notmuch)
  :config
  (setq notmuch-hello-sections
        '(notmuch-hello-insert-header
          notmuch-hello-insert-saved-searches
          ;; notmuch-hello-insert-search
          notmuch-hello-insert-alltags)
        notmuch-search-oldest-first nil
        notmuch-show-all-tags-list t
        notmuch-message-headers  ; see bug follow-up above
        '("Subject" "To" "Cc" "Date" "List-Id" "X-RT-Originator")
        notmuch-hello-thousands-separator ","
        notmuch-fcc-dirs
        '(("amin@aminb.org"            . "amin/Sent")
          ("abandali@uwaterloo.ca"     . "\"uwaterloo/Sent Items\"")
          ("amin.bandali@uwaterloo.ca" . "\"uwaterloo/Sent Items\"")
          ("aminb@gnu.org"             . "gnu/Sent")
          (".*"                        . "sent"))
        notmuch-search-result-format
        '(("date" . "%12s ")
          ("count" . "%-7s ")
          ("authors" . "%-40s ")
          ("subject" . "%s ")
          ("tags" . "(%s)")))
  ;; (add-hook 'visual-fill-column-mode-hook
  ;;           (lambda ()
  ;;             (when (string= major-mode 'notmuch-message-mode)
  ;;               (setq visual-fill-column-width 70))))
  ;; (set! :evil-state 'notmuch-message-mode 'insert)
  ;; (advice-add #'notmuch-bury-or-kill-this-buffer
  ;;             :override #'kill-this-buffer)
  :bind
  (:map notmuch-hello-mode-map
        ("g" . notmuch-poll-and-refresh-this-buffer)
        ("i" . (lambda ()
                 "Search for `inbox' tagged messages"
                 (interactive)
                 (notmuch-hello-search "tag:inbox")))
        ("u" . (lambda ()
                 "Search for `unread' tagged messages"
                 (interactive)
                 (notmuch-hello-search "tag:unread")))
        ("l" . (lambda ()
                 "Search for `latest tagged messages"
                 (interactive)
                 (notmuch-hello-search "tag:latest")))
        ("M" . (lambda ()
                 "Compose new mail and prompt for sender"
                 (interactive)
                 (let ((current-prefix-arg t))
                   (call-interactively #'notmuch-mua-new-mail)))))
  (:map notmuch-search-mode-map
        ("g" . notmuch-poll-and-refresh-this-buffer)
        ("k" . (lambda ()
                 "Mark message read"
                 (interactive)
                 (notmuch-search-tag '("-unread"))
                 ;; (notmuch-search-archive-thread)
                 (notmuch-search-next-thread)))
        ("u" . (lambda ()
                 "Mark message unread"
                 (interactive)
                 (notmuch-search-tag '("+unread"))
                 (notmuch-search-next-thread)))
        ("K" . (lambda ()
                 "Mark message deleted"
                 (interactive)
                 (notmuch-search-tag '("-unread" "-inbox" "+deleted"))
                 (notmuch-search-archive-thread)))
        ("S" . (lambda ()
                 "Mark message as spam"
                 (interactive)
                 (notmuch-search-tag '("-unread" "-inbox" "-webmasters" "+spam"))
                 (notmuch-search-archive-thread))))
  (:map notmuch-tree-mode-map  ; TODO: additional bindings
        ("S" . (lambda ()
                 "Mark message as spam"
                 (interactive)
                 (notmuch-tree-tag '("-unread" "-inbox" "-webmasters" "+spam"))
                 (notmuch-tree-archive-thread))))
)

;; (use-package counsel-notmuch
;;   :commands counsel-notmuch)

(after! notmuch-crypto
  (setq notmuch-crypto-process-mime t))

;; (after! evil
;;   (mapc (lambda (str) (evil-set-initial-state (car str) (cdr str)))
;;         '((notmuch-hello-mode . emacs)
;;           (notmuch-search-mode . emacs)
;;           (notmuch-tree-mode . emacs))))

(after! recentf
  (add-to-list 'recentf-exclude (expand-file-name ab-maildir)))

supercite

(use-package supercite
  :commands sc-cite-original
  :init
  (add-hook 'mail-citation-hook 'sc-cite-original)

  (defun sc-remove-existing-signature ()
    (save-excursion
      (goto-char (region-beginning))
      (when (re-search-forward message-signature-separator (region-end) t)
        (delete-region (match-beginning 0) (region-end)))))

  (add-hook 'mail-citation-hook 'sc-remove-existing-signature)

  (defun sc-remove-if-not-mailing-list ()
    (unless (assoc "list-id" sc-mail-info)
      (setq attribution sc-default-attribution
            citation (concat sc-citation-delimiter
                             sc-citation-separator))))

  (add-hook 'sc-attribs-postselect-hook 'sc-remove-if-not-mailing-list)

  :config
  (defun sc-fill-if-different (&optional prefix)
    "Fill the region bounded by `sc-fill-begin' and point.
Only fill if optional PREFIX is different than
`sc-fill-line-prefix'.  If `sc-auto-fill-region-p' is nil, do not
fill region.  If PREFIX is not supplied, initialize fill
variables.  This is useful for a regi `begin' frame-entry."
    (if (not prefix)
        (setq sc-fill-line-prefix ""
              sc-fill-begin (line-beginning-position))
      (if (and sc-auto-fill-region-p
               (not (string= prefix sc-fill-line-prefix)))
          (let ((fill-prefix sc-fill-line-prefix))
            (unless (or (string= fill-prefix "")
                        (save-excursion
                          (goto-char sc-fill-begin)
                          (or (looking-at ">+  +")
                              (< (length
                                  (buffer-substring (point)
                                                    (line-end-position)))
                                 65))))
              (fill-region sc-fill-begin (line-beginning-position)))
            (setq sc-fill-line-prefix prefix
                  sc-fill-begin (line-beginning-position)))))
nil))

Blogging

(use-package ox-hugo
  :after ox)

Post initialization

Display how long it took to load the init file.

(message "Loading %s...done (%.3fs)" user-init-file
         (float-time (time-subtract (current-time)
                                    ab--before-user-init-time)))

Footer

;;; init.el ends here