Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
2321 lines (1801 sloc) 78.6 KB

Emacs configuration

This is my Emacs configuration. It is pretty much like any other, with maybe a few interesting properties:

  • It uses a literate programming style with Org-Mode. Although the documentation is admittedly scarse, it makes the configuration easy to organize and understand.
  • It uses Borg instead of package.el or use-package. Borg has a lot of interesting properties compared to the alternatives: since packages (“drones”) are Git submodules, they can be modified and contributed back. Submodules also improve reproducibility, since you manually configure which version of which package you want installed.
  • It has a whole section dedicated to writing prose.
  • It uses Ivy and Helm. The bulk of completion is performed through Ivy, but in some rare cases where Helm is still required, or is a better fit for the job, Helm is used instead.

Contents

Introduction

This chapter deals with the general use of Emacs, and is limited to general settings and sane defaults. It’s a bit messy, since it’s mostly made up of all the bits that don’t fit anywhere else.

Let’s start by saying hello. Beyond being polite, when starting daemon it helps identifying when the literate configuration has started running.

(message "
███████╗███╗   ███╗ █████╗  ██████╗███████╗██╗
██╔════╝████╗ ████║██╔══██╗██╔════╝██╔════╝██║
█████╗  ██╔████╔██║███████║██║     ███████╗██║
██╔══╝  ██║╚██╔╝██║██╔══██║██║     ╚════██║╚═╝
███████╗██║ ╚═╝ ██║██║  ██║╚██████╗███████║██╗
╚══════╝╚═╝     ╚═╝╚═╝  ╚═╝ ╚═════╝╚══════╝╚═╝
")

And introduce ourselves:

(setq user-full-name    "Thibault Polge"
      user-mail-address "thibault@thb.lt")

For some reason, the default value of max-specpdl-size prevents Mu4e from correctly rendering some HTML e-mails. We increase it from 1300 to 5000.

(setq max-specpdl-size 5000)

Change the default major mode to text-mode instead of fundamental-mode. Fundamental has no hooks.

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

We want numbered backups, because catastrophes happen. The numbers may be a bit crazy, but better safe than sorry.

(setq version-control t
      kept-new-versions 500
      kept-old-versions 500)

Disable Customize by pointing it to /dev/null:

(setq custom-file "/dev/null")
(load custom-file t)

Use default browser from the system. Using setsid xdg-open prevents Emacs from killing xdg-open before it actually opened anything. See here.

(setq-default
 browse-url-browser-function 'browse-url-generic
 browse-url-generic-program "setsid"
 browse-url-generic-args '("xdg-open"))

Don’t lose the contents of system clipboard when killing from Emacs:

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

Running Emacs under MacOS requires a few adjustements:

(want-drone exec-path-from-shell)

(when (string= system-type 'darwin)
  ;; Don't use alt, cmd is meta
  (setq mac-option-modifier 'nil
        mac-command-modifier 'meta)

  ; Fix weird Apple keymap.on full-size kbs.
  (global-set-key (kbd "<help>") 'overwrite-mode)

  ; Fix load-path for mu4e (not sure this is still needed)
  (add-to-list 'load-path "/usr/local/share/emacs/site-lisp/mu4e")

  ; Load path from a shell
  (exec-path-from-shell-initialize))

User interface

(want-drones diminish
             general
             hydra
             visual-fill-column)

Settings and general configuration

(setq-default
 cursor-type '(bar . 5)
 enable-recursive-minibuffers t
 inhibit-startup-screen t
 use-dialog-box nil
 vc-follow-symlinks t

 truncate-lines t

 disabled-command-function nil)

Never use the “safe” yes-or-no function:

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

Don’t show the menu bar, unless this is MacOS. Never show toolbar or scrollbars.

(unless (string= 'system-type 'darwin) (menu-bar-mode -1))
(tool-bar-mode -1)
(scroll-bar-mode -1)

Mouse wheel scrolling makes big jumps by default, let’s make it smoother.

(setq mouse-wheel-scroll-amount '(1 ((shift) . 1)) ;; one line at a time
      mouse-wheel-progressive-speed nil ;; don't accelerate scrolling
      mouse-wheel-follow-mouse 't ;; scroll window under mouse

      scroll-step 1 ;; keyboard scroll one line at a time
      )

Rebind C-x k to kill the current buffer.

(global-set-key (kbd "C-x k") (lambda () (interactive) (kill-buffer (current-buffer))))

Fonts and themes

Configure the default font:

(add-to-list 'default-frame-alist '(font . "DejaVu Sans Mono"))
(set-face-attribute 'default nil
                    :height 60
                    )

And load the default theme: Eziam.

(want-drone eziam-theme-emacs)

(load-theme 'eziam-light t)

By default, multiple themes can be loaded at the same time. Nobody wants this (although it’s required by smart-mode-line)

(defadvice load-theme (before theme-dont-propagate activate)
  (mapc #'disable-theme custom-enabled-themes))

Create some shortcut commands to load both Eziam themes:

(defun eziam-dark () (interactive) (load-theme 'eziam-dark t))
(defun eziam-light () (interactive) (load-theme 'eziam-light t))

Modeline

(want-drone powerline)

(defun thblt/powerline-set-faces (&rest args)
  (let* ((default-bg (face-attribute 'default :background))
         (default-fg (face-attribute 'default :foreground))

         ;; FIXME This is NOT a way to compute brightness.  Average the three components.
         (dark (< (string-to-number (substring default-bg 1) 16) #x7FFFFF)))

    (face-spec-set 'mode-line
                   `((t :background ,default-bg :foreground ,default-fg)))

    (face-spec-set 'mode-line-inactive
                   `((t :background ,default-fg :foreground ,default-bg)))


    (face-spec-set 'thblt/powerline-transparent-face
                   `((t :background ,default-bg :foreground ,default-fg)))

    (face-spec-set 'thblt/powerline-window-information-active-face
                   `((t :background "orange" :foreground "black" :weight bold)))
    (face-spec-set 'thblt/powerline-window-information-face
                   `((t :background ,default-bg :foreground "DarkOrange" :weight bold)))

    (face-spec-set 'thblt/powerline-persp-active-face
                   `((t :background "DarkViolet" :foreground "white")))
    (face-spec-set 'thblt/powerline-persp-face
                   `((t :background ,default-bg :foreground "DarkViolet")))
    (face-spec-set 'thblt/powerline-persp-active-bad-face
                   `((t :background "red")))
    (face-spec-set 'thblt/powerline-persp-bad-face
                   `((t :background ,default-bg :foreground "red")))

    (face-spec-set 'thblt/powerline-buffer-id-active-face
                   `((t :background ,default-fg :foreground ,default-bg :weight bold)))
    (face-spec-set 'thblt/powerline-buffer-id-face
                   `((t :background ,default-bg :foreground ,default-fg)))

    (face-spec-set 'thblt/powerline-buffer-read-only-face
                   `((t :inherit thblt/powerline-buffer-id-face :foreground "red")))
    (face-spec-set 'thblt/powerline-buffer-read-only-active-face
                   `((t :inherit thblt/powerline-buffer-id-active-face :foreground "red"))))

  (when (fboundp 'powerline-reset) (powerline-reset)))

(thblt/powerline-set-faces)
(advice-add 'load-theme :after 'thblt/powerline-set-faces)

(defun thblt/powerline-get-face (base &optional variant)
  "Select a face for the mode-line."
  (intern (format "thblt/powerline-%s%s%s-face"
                  base
                  (if active "-active" "")
                  (if variant (concat "-" variant) ""))))

(defvar thblt/diminished-major-modes
  nil
  "A list of (MAJOR-MODE . REPR)")

(setq thblt/diminished-major-modes
      '((emacs-lisp-mode . "EL")
        (erc-mode . nil)))

(defvar thblt/languages-reprs
  `(("fr" ,(concat
            (propertize " " 'face '(:background "blue"))
            (propertize " " 'face '(:background "white"))
            (propertize " " 'face '(:background "red")))))
  "An Alist of language IDs and representations"
  )

(require 'ace-window) ;; We call (aw-update) when updating the
;; mode-line.

(setq-default mode-line-format
              '("%e"
                (:eval
                 (cl-flet ((w (str) (if str (concat " " str " ") "")))
                   (let* ((active (powerline-selected-window-active))
                          (mode-line-buffer-id (if active 'mode-line-buffer-id 'mode-line-buffer-id-inactive))
                          (mode-line (if active 'mode-line 'mode-line-inactive))
                          (face1 (if active 'powerline-active1 'powerline-inactive1))
                          (face2 (if active 'powerline-active2 'powerline-inactive2))
                          (standard-separator "chamfer")
                          (special-separator "butt")
                          (separator-left (intern (format "powerline-%s-%s"
                                                          standard-separator
                                                          (car powerline-default-separator-dir))))
                          (separator-right (intern (format "powerline-%s-%s"
                                                           standard-separator
                                                           (cdr powerline-default-separator-dir))))
                          (special-separator-left (intern (format "powerline-%s-%s"
                                                                  special-separator
                                                                  (car powerline-default-separator-dir))))
                          (special-separator-right (intern (format "powerline-%s-%s"
                                                                   special-separator
                                                                   (cdr powerline-default-separator-dir))))

                          (face)
                          (lhs (list
                                ;; Window ID
                                (powerline-raw
                                 (w (progn
                                      (aw-update)
                                      (let ((path (window-parameter (selected-window) 'ace-window-path)))
                                        (set-text-properties 0 1 nil path)
                                        path)))
                                 (setq face (thblt/powerline-get-face "window-information")))
                                ;;
                                ;; Perspective
                                (funcall separator-left face
                                         (setq face (thblt/powerline-get-face "persp" (unless (persp-contain-buffer-p) "bad"))))
                                (powerline-raw
                                 (w (safe-persp-name (get-frame-persp)))
                                 face)
                                ;;
                                ;; Buffer ID
                                (funcall separator-left face (setq face 'thblt/powerline-transparent-face))
                                (powerline-raw " " face)

                                (funcall special-separator-right face
                                         (setq face (thblt/powerline-get-face "buffer-id")))
                                (when buffer-read-only
                                  (powerline-raw "" (thblt/powerline-get-face "buffer-read-only")))
                                (powerline-raw
                                 (w (buffer-name)) face)
                                ;;
                                ;; Modes
                                (funcall special-separator-left face (setq face 'thblt/powerline-transparent-face))
                                (powerline-raw " " face)
                                (funcall special-separator-left face face1)

                                (let ((major-mode-repr (if (assoc major-mode thblt/diminished-major-modes)
                                                           (alist-get major-mode thblt/diminished-major-modes)
                                                         mode-name))
                                      (minor-modes-repr (format-mode-line minor-mode-alist)))

                                  (when (or major-mode-repr minor-modes-repr)
                                    (powerline-raw
                                     (concat
                                      major-mode-repr
                                      minor-modes-repr))))


                                (powerline-process face1)
                                ;;(powerline-minor-modes face1 'l)
                                (powerline-narrow face1 'l)
                                (funcall separator-left face1 face2)
                                ))

                          (rhs (list
                                (powerline-raw global-mode-string face2)
                                (funcall separator-right face2 face1)
                                (unless window-system
                                  (powerline-raw (char-to-string #xe0a1) face1 'l))
                                (powerline-raw "%3l" face1 'l)
                                (powerline-raw ":" face1)
                                (powerline-raw "%2c" face1 'r)
                                (funcall separator-right face1 mode-line)
                                (powerline-raw " ")
                                )))
                     (concat (powerline-render lhs)
                             (powerline-fill mode-line (powerline-width rhs))
                             (powerline-render rhs)))))))

Perspectives (persp-mode)

(want-drone persp-mode)

(setq persp-auto-resume-time -1
      persp-kill-foreign-buffer-action 'kill
      persp-autokill-persp-when-removed-last-buffer 'kill)

(general-define-key
 "C-x b" 'persp-switch-to-buffer)

(persp-mode)
(diminish 'persp-mode)

Project management with Projectile

Let’s load Projectile, and:

  • globally ignore undo-files and similar byproducts.
  • toggle the C-p p and C-p SPC bindings (I find the latter easier to enter, and thus more adequate for “do what I mean”);

TODO:

  • Could Projectile read ignore patterns from ~/.gitignore_global?
(want-drones projectile
             counsel-projectile)

(projectile-global-mode)
(counsel-projectile-on)

(setq projectile-globally-ignored-file-suffixes (append '(
                                                          ".un~"
                                                          ".~undo-tree~"
                                                          )
                                                        projectile-globally-ignored-files))

(diminish 'projectile-mode)

I consider submodules to be separate projects, so don’t include then in the main file listing:

(setq projectile-git-submodule-command nil)

Projectile and persp-mode

Automatic perspective creation:

(defun thblt/project-name-to-persp-name (name)
  "Build a perspective name from project name NAME."
  (concat "p) " name))

(defun thblt/project-path-from-persp-name (name)
  "Retrieve a project path from persp name NAME."
  (setq name (substring name 3))
  (car (cl-remove-if-not
        (lambda (p) (equal (funcall projectile-project-name-function p) name)) projectile-known-projects)))

(with-eval-after-load 'persp-mode
  (defvar persp-mode-projectile-bridge-before-switch-selected-window-buffer nil)

  ;; (setq persp-add-buffer-on-find-file 'if-not-autopersp)

  (persp-def-auto-persp "projectile"
                        :parameters '((dont-save-to-file . t)
                                      (persp-mode-projectile-bridge . t))
                        :hooks '(projectile-before-switch-project-hook
                                 projectile-after-switch-project-hook
                                 projectile-find-file-hook
                                 find-file-hook)
                        :dyn-env '((after-switch-to-buffer-adv-suspend t))
                        :switch 'frame
                        :predicate
                        #'(lambda (buffer &optional state)
                            (if (eq 'projectile-before-switch-project-hook
                                    (alist-get 'hook state))
                                state
                              (and
                               projectile-mode
                               (buffer-live-p buffer)
                               (or
                                (buffer-file-name buffer)
                                (string-prefix-p "magit" (symbol-name (buffer-local-value 'major-mode buffer))))
                               ;; (not git-commit-mode)
                               (projectile-project-p)
                               (or state t))))
                        :get-name
                        #'(lambda (state)
                            (if (eq 'projectile-before-switch-project-hook
                                    (alist-get 'hook state))
                                state
                              (push (cons 'persp-name
                                          (thblt/project-name-to-persp-name
                                           (with-current-buffer (alist-get 'buffer state)
                                             (projectile-project-name))))
                                    state)
                              state))
                        :on-match
                        #'(lambda (state)
                            (let ((hook (alist-get 'hook state))
                                  (persp (alist-get 'persp state))
                                  (buffer (alist-get 'buffer state)))
                              (case hook
                                (projectile-before-switch-project-hook
                                 (let ((win (if (minibuffer-window-active-p (selected-window))
                                                (minibuffer-selected-window)
                                              (selected-window))))
                                   (when (window-live-p win)
                                     (setq persp-mode-projectile-bridge-before-switch-selected-window-buffer
                                           (window-buffer win)))))

                                (projectile-after-switch-project-hook
                                 (when (buffer-live-p
                                        persp-mode-projectile-bridge-before-switch-selected-window-buffer)
                                   (let ((win (selected-window)))
                                     (unless (eq (window-buffer win)
                                                 persp-mode-projectile-bridge-before-switch-selected-window-buffer)
                                       (set-window-buffer
                                        win persp-mode-projectile-bridge-before-switch-selected-window-buffer)))))

                                (find-file-hook
                                 (setcdr (assq :switch state) nil)))
                              (if (case hook
                                    (projectile-before-switch-project-hook nil)
                                    (t t))
                                  (persp--auto-persp-default-on-match state)
                                (setcdr (assq :after-match state) nil)))
                            state)
                        :after-match
                        #'(lambda (state)
                            (when (eq 'find-file-hook (alist-get 'hook state))
                              (run-at-time 0.5 nil
                                           #'(lambda (buf persp)
                                               (when (and (eq persp (get-current-persp))
                                                          (not (eq buf (window-buffer (selected-window)))))
                                                 ;; (switch-to-buffer buf)
                                                 (persp-add-buffer buf persp t nil)))
                                           (alist-get 'buffer state)
                                           (get-current-persp)))
                            (persp--auto-persp-default-after-match state))))

Context switching

This section essentially provides tightier integration between Persp-Mode and Projectile.

First we create a “context switcher” which allows to switch to an existing perspective, an opened project or a known, but closed, project. It basically merges persp-switch and counsel-projectile-switch-projectile.

(defvar thblt/context-starters
  nil
  "A list of (CONTEXT-NAME . COMMAND).

CONTEXT-NAME is the name of an automatic perspective.
COMMAND is the command used to start this perspective.")

(defun thblt/context-switch (context)
  "Switch to CONTEXT."
  (interactive "i")
  (let* ((persps (mapcar #'safe-persp-name (persp-persps)))
         (projects (cl-remove-if
                    (lambda (p) (member p persps))
                    (mapcar (lambda (p)
                              (thblt/project-name-to-persp-name
                               (funcall projectile-project-name-function p)))
                            projectile-known-projects)))
         (starters (cl-remove-if
                    (lambda (p) (member p persps))
                    (mapcar 'car thblt/context-starters))))

    (unless context
      (setq context
            (ivy-completing-read "Switch to context: "
                                 (append persps projects starters))))
    (cond ((member context persps)
           (persp-frame-switch context))
          ((member context projects)
           (projectile-switch-project-by-name (thblt/project-path-from-persp-name context)))
          ((member context starters)
           (funcall (cdr (assoc context thblt/context-starters))))
          (t (error "No such perspective, project or context starter %s." context)))))

And now for some bindings:

(general-define-key
 :keymaps 'projectile-mode-map
 :prefix projectile-keymap-prefix
 "A" (lambda () (interactive) (persp-add-buffer (current-buffer)))
 "p" 'thblt/context-switch
 "Z" 'persp-auto-persps-pickup-buffers)

UI Utilities

Ace-window

(want-drone ace-window)

(with-eval-after-load 'ace-window
  ;; We make use of aw-ignored-buffers, so we need the eval-after-load
  (setq aw-scope 'frame
        aw-background nil

        aw-ignore-on t

        aw-ignored-buffers (append aw-ignored-buffers
                                   (mapcar (lambda (n) (format " *Minibuf-%s*" n))
                                           (number-sequence 0 20)))))

(defun thblt/aw-switch-to-numbered-window (number)
  (aw-switch-to-window (nth (- number 1) (aw-window-list))))

(defun thblt/switch-to-minibuffer ()
  "Switch to minibuffer window."
  (interactive)
  (if (active-minibuffer-window)
      (select-window (active-minibuffer-window))
    (error "Minibuffer is not active")))

(general-define-key "C-x o" 'ace-window
                    ;; Emulate window-numbering
                    "M-0" 'thblt/switch-to-minibuffer)
                    ;; "M-1" (lambda () (interactive) (thblt/aw-switch-to-numbered-window 1))
                    ;; "M-2" (lambda () (interactive) (thblt/aw-switch-to-numbered-window 2))
                    ;; "M-3" (lambda () (interactive) (thblt/aw-switch-to-numbered-window 3))
                    ;; "M-4" (lambda () (interactive) (thblt/aw-switch-to-numbered-window 4))
                    ;; "M-5" (lambda () (interactive) (thblt/aw-switch-to-numbered-window 5))
                    ;; "M-6" (lambda () (interactive) (thblt/aw-switch-to-numbered-window 6))
                    ;; "M-7" (lambda () (interactive) (thblt/aw-switch-to-numbered-window 7))
                    ;; "M-8" (lambda () (interactive) (thblt/aw-switch-to-numbered-window 8))
                    ;; "M-9" (lambda () (interactive) (thblt/aw-switch-to-numbered-window 9)))

Buffer management (ibuffer)

TODO Is this still needed with Persp?

Rebind C-x C-b to ibuffer instead of list-buffers:

(global-set-key (kbd "C-x C-b") 'ibuffer)

Eyebrowse

(eyebrowse-mode)

Ivy

(want-drone ivy)

(setq ivy-use-virtual-buffers t)

(ivy-mode)
(diminish 'ivy-mode)

(general-define-key
         "M-i"     'counsel-imenu
         "M-x"     'counsel-M-x
         "C-x C-f" 'counsel-find-file

         "C-S-s"   'swiper

         "C-x 8 RET" 'counsel-unicode-char)

Popwin

Popwin “makes you free from the hell of annoying buffers”:

(want-drone popwin)

(require 'popwin)
(popwin-mode)

Which-key

(want-drone which-key)

(which-key-mode)
(diminish 'which-key-mode)

Customization helper

A little function to identify the face at point. Nice to have when writing themes, and faster than C-u C-x =.

(defun what-face (pos)
  (interactive "d")
  (let ((face (or (get-char-property (point) 'read-face-name)
                  (get-char-property (point) 'face))))
    (if face (message "Face: %s" face) (message "No face at %d" pos))))

Editing text

This chapter deals with general text editing. The next two configure prose and code editing, respectively.

Spell checking

(want-drone auto-dictionary)

Use aspell instead of ispell:

(setq ispell-program-name "aspell")

Don’t ask before saving custom dict:

(setq ispell-silently-savep t)

And enable Flyspell:

(add-hook 'text-mode-hook (lambda () (flyspell-mode t)))

(diminish 'flyspell-mode "Fly")

Disable horrible and confusing Flyspell “duplicate” marks. These are easily confused with actually misspelled words, but M-$ won’t work on them, and would “correct” another word, possibly off-screen.

(setq flyspell-duplicate-distance 0)

Correct words using Ivy instead of default method:

(want-drone flyspell-correct)
(require 'flyspell-correct-ivy)

(general-define-key :keymaps 'flyspell-mode-map
                    "M-$" 'flyspell-auto-correct-previous-word
                    "C-;" 'flyspell-correct-previous-word-generic)

Auto-dictionary mode. Disabled for now, as it seems to slow everything down + doesn’t work with org-mode.

(add-hook 'flyspell-mode-hook (lambda () (auto-dictionary-mode)))

“Modal” editing

Selected is a package which allows to create specific bindings when region is active:

(want-drone selected)

(defvar selected-org-mode-map (make-sparse-keymap))
(selected-global-mode)
(diminish 'selected-minor-mode)

Moving around

beginend

(require 'beginend)
(beginend-global-mode)
 (mapc (lambda (m) (diminish (cdr m)))
      beginend-modes)
(diminish 'beginend-global-mode)

mwim

(global-set-key (kbd "C-a") 'mwim-beginning-of-code-or-line)
(global-set-key (kbd "C-e") 'mwim-end-of-code-or-line)
(global-set-key (kbd "<home>") 'mwim-beginning-of-line-or-code)
(global-set-key (kbd "<end>") 'mwim-end-of-line-or-code)

nav-flash (don’t get lost)

(require 'nav-flash)

(face-spec-set 'nav-flash-face '((t (:inherit pulse-highlight-face))))

(advice-add 'recenter-top-bottom :after (lambda (x) (nav-flash-show)))

Replace

(want-drone visual-regexp)

(general-define-key
         "C-M-%" 'vr/query-replace
         "C-c r" 'vr/replace
         "C-c m" 'vr/mc-mark)

Minor modes

Auto-revert-mode

(with-eval-after-load 'autorevert
  (diminish 'auto-revert-mode ""))

Expand-region

(want-drone expand-region)

Move text

Move lines of text with M-<up> and M-<down>.

(want-drone move-text)

(move-text-default-bindings)

Multiple cursors

(want-drone multiple-cursors)

(add-hook 'prog-mode-hook (lambda () (multiple-cursors-mode t)))
(add-hook 'text-mode-hook (lambda () (multiple-cursors-mode t)))
(general-define-key "C-S-c C-S-c" 'mc/edit-lines)

Recentf

(recentf-mode)

Smartparens

(want-drone smartparens)
(require 'smartparens-config) ;; Load default config

(smartparens-global-mode)
(show-smartparens-global-mode)

(diminish 'smartparens-mode)

Bindings

I’m stealing and modifying smartparens’ author config:

(add-hook 'minibuffer-setup-hook 'turn-on-smartparens-strict-mode)


(general-define-key :map smartparens-mode-map
                    "C-M-f" 'sp-forward-sexp

                    "C-M-b" 'sp-backward-sexp

                    "C-M-d" 'sp-down-sexp
                    "C-M-a" 'sp-backward-down-sexp
                    "C-S-d" 'sp-beginning-of-sexp
                    "C-S-a" 'sp-end-of-sexp

                    "C-M-e" 'sp-up-sexp
                    "C-M-u" 'sp-backward-up-sexp
                    "C-M-t" 'sp-transpose-sexp

                    "C-M-n" 'sp-next-sexp
                    "C-M-p" 'sp-previous-sexp

                    "C-M-k" 'sp-kill-sexp
                    "C-M-w" 'sp-copy-sexp

                    "M-<delete>" 'sp-unwrap-sexp
                    "M-<backspace>" 'sp-backward-unwrap-sexp

                    "C-<right>" 'sp-forward-slurp-sexp
                    "C-<left>" 'sp-forward-barf-sexp
                    "C-M-<left>" 'sp-backward-slurp-sexp
                    "C-M-<right>" 'sp-backward-barf-sexp

                    "M-D" 'sp-splice-sexp
                    "C-M-<delete>" 'sp-splice-sexp-killing-forward
                    "C-M-<backspace>" 'sp-splice-sexp-killing-backward
                    "C-S-<backspace>" 'sp-splice-sexp-killing-around

                    "C-]" 'sp-select-next-thing-exchange
                    "C-<left_bracket>" 'sp-select-previous-thing
                    "C-M-]" 'sp-select-next-thing

                    "M-F" 'sp-forward-symbol
                    "M-B" 'sp-backward-symbol

                    "C-c f" (lambda () (interactive) (sp-beginning-of-sexp 2))
                    "C-c b" (lambda () (interactive) (sp-beginning-of-sexp -2))

                    "C-M-s"
                    (defhydra smartparens-hydra ()
                      "Smartparens"
                      ("d" sp-down-sexp "Down")
                      ("e" sp-up-sexp "Up")
                      ("u" sp-backward-up-sexp "Up")
                      ("a" sp-backward-down-sexp "Down")
                      ("f" sp-forward-sexp "Forward")
                      ("b" sp-backward-sexp "Backward")
                      ("k" sp-kill-sexp "Kill" :color blue)
                      ("q" nil "Quit" :color blue)))


;; (bind-key "H-t" 'sp-prefix-tag-object smartparens-mode-map)
;; (bind-key "H-p" 'sp-prefix-pair-object smartparens-mode-map)
;; (bind-key "H-y" 'sp-prefix-symbol-object smartparens-mode-map)
;; (bind-key "H-h" 'sp-highlight-current-sexp smartparens-mode-map)
;; (bind-key "H-e" 'sp-prefix-save-excursion smartparens-mode-map)
;; (bind-key "H-s c" 'sp-convolute-sexp smartparens-mode-map)
;; (bind-key "H-s a" 'sp-absorb-sexp smartparens-mode-map)
;; (bind-key "H-s e" 'sp-emit-sexp smartparens-mode-map)
;; (bind-key "H-s p" 'sp-add-to-previous-sexp smartparens-mode-map)
;; (bind-key "H-s n" 'sp-add-to-next-sexp smartparens-mode-map)
;; (bind-key "H-s j" 'sp-join-sexp smartparens-mode-map)
;; (bind-key "H-s s" 'sp-split-sexp smartparens-mode-map)
;; (bind-key "H-s r" 'sp-rewrap-sexp smartparens-mode-map)
;; (defvar hyp-s-x-map)
;; (define-prefix-command 'hyp-s-x-map)
;; (bind-key "H-s x" hyp-s-x-map smartparens-mode-map)
;; (bind-key "H-s x x" 'sp-extract-before-sexp smartparens-mode-map)
;; (bind-key "H-s x a" 'sp-extract-after-sexp smartparens-mode-map)
;; (bind-key "H-s x s" 'sp-swap-enclosing-sexp smartparens-mode-map)

;; (bind-key "C-x C-t" 'sp-transpose-hybrid-sexp smartparens-mode-map)

;; (bind-key ";" 'sp-comment emacs-lisp-mode-map)

;; (bind-key [remap c-electric-backspace] 'sp-backward-delete-char smartparens-strict-mode-map)

;; ;;;;;;;;;;;;;;;;;;
;; ;; pair management

;; (sp-local-pair 'minibuffer-inactive-mode "'" nil :actions nil)
;; (bind-key "C-(" 'sp---wrap-with-40 minibuffer-local-map)

;; ;;; markdown-mode
;; (sp-with-modes '(markdown-mode gfm-mode rst-mode)
;;   (sp-local-pair "*" "*"
;;                  :wrap "C-*"
;;                  :unless '(sp--gfm-point-after-word-p sp-point-at-bol-p)
;;                  :post-handlers '(("[d1]" "SPC"))
;;                  :skip-match 'sp--gfm-skip-asterisk)
;;   (sp-local-pair "**" "**")
;;   (sp-local-pair "_" "_" :wrap "C-_" :unless '(sp-point-after-word-p)))

;; (defun sp--gfm-point-after-word-p (id action context)
;;   "Return t if point is after a word, nil otherwise.
;; This predicate is only tested on \"insert\" action."
;;   (when (eq action 'insert)
;;     (sp--looking-back-p (concat "\\(\\sw\\)" (regexp-quote id)))))

;; (defun sp--gfm-skip-asterisk (ms mb me)
;;   (save-excursion
;;     (goto-char mb)
;;     (save-match-data (looking-at "^\\* "))))

;; ;;; rst-mode
;; (sp-with-modes 'rst-mode
;;   (sp-local-pair "``" "``"))

;; ;;; org-mode
;; (sp-with-modes 'org-mode
;;   (sp-local-pair "*" "*" :actions '(insert wrap) :unless '(sp-point-after-word-p sp-point-at-bol-p) :wrap "C-*" :skip-match 'sp--org-skip-asterisk)
;;   (sp-local-pair "_" "_" :unless '(sp-point-after-word-p) :wrap "C-_")
;;   (sp-local-pair "/" "/" :unless '(sp-point-after-word-p) :post-handlers '(("[d1]" "SPC")))
;;   (sp-local-pair "~" "~" :unless '(sp-point-after-word-p) :post-handlers '(("[d1]" "SPC")))
;;   (sp-local-pair "=" "=" :unless '(sp-point-after-word-p) :post-handlers '(("[d1]" "SPC")))
;;   (sp-local-pair "«" "»"))

;; (defun sp--org-skip-asterisk (ms mb me)
;;   (or (and (= (line-beginning-position) mb)
;;            (eq 32 (char-after (1+ mb))))
;;       (and (= (1+ (line-beginning-position)) me)
;;            (eq 32 (char-after me)))))

;; ;;; tex-mode latex-mode
;; (sp-with-modes '(tex-mode plain-tex-mode latex-mode)
;;   (sp-local-tag "i" "\"<" "\">"))

;; ;;; lisp modes
;; (sp-with-modes sp--lisp-modes
;;   (sp-local-pair "(" nil
;;                  :wrap "C-("
;;                  :pre-handlers '(my-add-space-before-sexp-insertion)
;;                  :post-handlers '(my-add-space-after-sexp-insertion)))



;; (defun my-add-space-after-sexp-insertion (id action _context)
;;   (when (eq action 'insert)
;;     (save-excursion
;;       (forward-char (sp-get-pair id :cl-l))
;;       (when (or (eq (char-syntax (following-char)) ?w)
;;                 (looking-at (sp--get-opening-regexp)))
;;         (insert " ")))))

;; (defun my-add-space-before-sexp-insertion (id action _context)
;;   (when (eq action 'insert)
;;     (save-excursion
;;       (backward-char (length id))
;;       (when (or (eq (char-syntax (preceding-char)) ?w)
;;                 (and (looking-back (sp--get-closing-regexp))
;;                      (not (eq (char-syntax (preceding-char)) ?'))))
;;         (insert " ")))))

;; ;;; C++
;; (sp-with-modes '(malabar-mode c++-mode)
;;   (sp-local-pair "{" nil :post-handlers '(("||\n[i]" "RET"))))
;; (sp-local-pair 'c++-mode "/*" "*/" :post-handlers '((" | " "SPC")
;;                                                     ("* ||\n[i]" "RET")))

;; ;;; PHP
;; (sp-with-modes '(php-mode)
;;   (sp-local-pair "/**" "*/" :post-handlers '(("| " "SPC")
;;                                              (my-php-handle-docstring "RET")))
;;   (sp-local-pair "/*." ".*/" :post-handlers '(("| " "SPC")))
;;   (sp-local-pair "{" nil :post-handlers '(("||\n[i]" "RET")))
;;   (sp-local-pair "(" nil :prefix "\\(\\sw\\|\\s_\\)*"))

;; (defun my-php-handle-docstring (&rest _ignored)
;;   (-when-let (line (save-excursion
;;                      (forward-line)
;;                      (thing-at-point 'line)))
;;     (cond
;;      ;; variable
;;      ((string-match (rx (or "private" "protected" "public" "var") (1+ " ") (group "$" (1+ alnum))) line)
;;       (let ((var-name (match-string 1 line))
;;             (type ""))
;;         ;; try to guess the type from the constructor
;;         (-when-let (constructor-args (my-php-get-function-args "__construct" t))
;;           (setq type (or (cdr (assoc var-name constructor-args)) "")))
;;         (insert "* @var " type)
;;         (save-excursion
;;           (insert "\n"))))
;;      ((string-match-p "function" line)
;;       (save-excursion
;;         (let ((args (save-excursion
;;                       (forward-line)
;;                       (my-php-get-function-args nil t))))
;;           (--each args
;;             (when (my-php-should-insert-type-annotation (cdr it))
;;               (insert (format "* @param %s%s\n"
;;                               (my-php-translate-type-annotation (cdr it))
;;                               (car it))))))
;;         (let ((return-type (save-excursion
;;                              (forward-line)
;;                              (my-php-get-function-return-type))))
;;           (when (my-php-should-insert-type-annotation return-type)
;;             (insert (format "* @return %s\n" (my-php-translate-type-annotation return-type))))))
;;       (re-search-forward (rx "@" (or "param" "return") " ") nil t))
;;      ((string-match-p ".*class\\|interface" line)
;;       (save-excursion (insert "\n"))
;;       (insert "* ")))
;;     (let ((o (sp--get-active-overlay)))
;;       (indent-region (overlay-start o) (overlay-end o)))))

Extra pairs

Stolen this list from xah-fly-keys:

(sp-pair "(" ")")
(sp-pair "[" "]")
(sp-pair "{" "}")
(sp-pair "<" ">")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "«" "»")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "︿" "")
(sp-pair "" "")
(sp-pair "" "")
(sp-pair "" "")

Undo-tree

(want-drone undo-tree)

(setq undo-tree-auto-save-history t
      undo-tree-visualizer-diff t)

(global-undo-tree-mode)
(diminish 'undo-tree-mode)

Unfill

(want-drone unfill)

(define-key selected-keymap (kbd "M-Q") 'unfill-region)

Yasnippet

(want-drone yasnippet)

(yas-global-mode)
(diminish 'yas-minor-mode)

Misc customizations

Use C-h as backspace

(general-define-key "C-h" 'delete-backward-char)

Autosave when losing focus

This is the initial version, which works perfectly well:

(add-hook 'focus-out-hook
          (lambda ()
            (save-some-buffers t)))

Delete trailing whitespace when saving

(add-hook 'before-save-hook 'delete-trailing-whitespace)

Diff files before marking a buffer modified

Ignore modification-time-only changes in files, i.e. ones that don’t really change the contents. This happens often with switching between different VC buffers. Code comes from this StackOverflow question.

(defun update-buffer-modtime-if-byte-identical ()
  (let* ((size      (buffer-size))
         (byte-size (position-bytes size))
         (filename  buffer-file-name))
    (when (and byte-size (<= size 1000000))
      (let* ((attributes (file-attributes filename))
             (file-size  (nth 7 attributes)))
        (when (and file-size
                   (= file-size byte-size)
                   (string= (buffer-substring-no-properties 1 (1+ size))
                            (with-temp-buffer
                              (insert-file-contents filename)
                              (buffer-string))))
          (set-visited-file-modtime (nth 5 attributes))
          t)))))

(defun verify-visited-file-modtime--ignore-byte-identical (original &optional buffer)
  (or (funcall original buffer)
      (with-current-buffer buffer
        (update-buffer-modtime-if-byte-identical))))
(advice-add 'verify-visited-file-modtime :around #'verify-visited-file-modtime--ignore-byte-identical)

(defun ask-user-about-supersession-threat--ignore-byte-identical (original &rest arguments)
  (unless (update-buffer-modtime-if-byte-identical)
    (apply original arguments)))
(advice-add 'ask-user-about-supersession-threat :around #'ask-user-about-supersession-threat--ignore-byte-identical)

Writing prose

This section deals with two things:

  1. Major modes dedicated to writing prose, as opposed to code or configuration.
  2. Non-code bits in code/configuration files: comments and integrated documentation.

The text-mode hydra

TODO validate : and = on all keyboard mappings.

(setq visual-fill-column-width fill-column)

(defhydra hydra-text-mode ()
  "text-mode switches"
  ("f" flyspell-mode "Flyspell")
  ("d" ispell-change-dictionary "Language")
  ("w" visual-fill-column-mode "Visual fill column")
  ("," text-scale-decrease "Decrease font size")
  (";" text-scale-increase "Increase font size")
  (":" (lambda () (interactive) (setq-local visual-fill-column-width (- visual-fill-column-width 5))) "Decrease width")
  ("!" (lambda () (interactive) (setq-local visual-fill-column-width (+ visual-fill-column-width 5))) "Decrease width"))


(general-define-key :keymaps 'text-mode-map
                    "C-x w" 'hydra-text-mode/body)

Common settings and minor modes

Abbrev

(add-hook 'text-mode-hook (lambda () (abbrev-mode t)))
(diminish 'abbrev-mode)

Unfill

(want-drone unfill)
(general-define-key "M-Q" 'unfill-paragraph)

Wordwrap/visual line/visual-fill-column

(with-eval-after-load 'simple
  (diminish 'visual-line-mode))

(want-drone visual-fill-column)
(require 'visual-fill-column)

(dolist (hook '(markdown-mode-hook org-mode-hook))
  (add-hook hook (lambda () (setq visual-fill-column-center-text t))))

Major modes

(want-drone markdown-mode)

AucTex

(want-drones auctex
             company-auctex)

(add-hook 'LaTeX-mode-hook (lambda ()
                             (visual-line-mode t)
                             (TeX-fold-mode t)))

(progn
  (setq-default TeX-save-query nil      ; Autosave
                TeX-parse-self t
                TeX-engine 'xetex
                TeX-source-correlate-mode t)) ;; Synctex on

(with-eval-after-load 'reftex-vars
  (progn
    ;; (also some other reftex-related customizations)
    (setq reftex-cite-format
          '((?\C-m . "\\cite[]{%l}")
            (?f . "\\footcite[][]{%l}")
            (?t . "\\textcite[q]{%l}")
            (?p . "\\parencite[]{%l}")
            (?o . "\\citepr[]{%l}")
            (?n . "\\nocite{%l}")))))

Org-mode

(want-drone htmlize
            org
            org-download)

(setq org-catch-invisible-edits t ; Avoid editing folded contents
      org-hide-leading-stars t
      org-hide-emphasis-markers t
      org-html-htmlize-output-type 'css ; Use CSS selectors
                                        ; instead of inline
                                        ; styles in
                                        ; generated HTML
                                        ; code blocks
      org-imenu-depth 6
      org-src-fontify-natively t  ; Syntax highlighting in src blocks.
      )
(add-hook 'org-mode-hook (lambda ()
                           (org-indent-mode t)
                           (visual-line-mode t)
                           (which-function-mode t)))

(with-eval-after-load 'org-indent
  (diminish 'org-indent-mode)
  )

Configure smartparens:

(sp-with-modes 'org-mode
  (sp-local-pair "*" "*" :actions '(insert wrap) :unless '(sp-point-after-word-p sp-point-at-bol-p) :wrap "C-*" :skip-match 'sp--org-skip-asterisk)
  (sp-local-pair "_" "_" :unless '(sp-point-after-word-p) :wrap "C-_")
  (sp-local-pair "/" "/" :unless '(sp-point-after-word-p) :post-handlers '(("[d1]" "SPC")))
  (sp-local-pair "~" "~" :unless '(sp-point-after-word-p) :post-handlers '(("[d1]" "SPC")))
  (sp-local-pair "=" "=" :unless '(sp-point-after-word-p) :post-handlers '(("[d1]" "SPC"))))

(defun sp--org-skip-asterisk (ms mb me)
  (or (and (= (line-beginning-position) mb)
           (eq 32 (char-after (1+ mb))))
      (and (= (1+ (line-beginning-position)) me)
           (eq 32 (char-after me)))))

Some cool org extensions:

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

Identify position in buffer:

(defun thblt/org-where-am-i ()
  "Return a string of headers indicating where point is in the current tree."
  (interactive)
  (let (headers)
    (save-excursion
(while (condition-case nil
     (progn
       (push (nth 4 (org-heading-components)) headers)
       (outline-up-heading 1))
   (error nil))))
(message (mapconcat #'identity headers " > "))))

(general-define-key :keymaps 'org-mode-map
                    "<f1> <f1>" 'thblt/org-where-am-i)

The emphasize selected bindings:

(define-key selected-org-mode-map (kbd "b") (lambda () (interactive) (org-emphasize ?*)))
(define-key selected-org-mode-map (kbd "i") (lambda () (interactive) (org-emphasize ?/)))

Org-agenda:

(setq org-agenda-files (list "~/Documents/LOG.org")
      org-default-notes-file "~/Documents/LOG.org")

Org-babel

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

Org-ref

(want-drone org-ref)

(setq org-ref-completion-library 'org-ref-ivy-cite)

Writing code

Settings

Some basic settings…

(setq-default comment-empty-lines nil
	        tab-width 2
	        c-basic-offset 2
	        cperl-indent-level 2
	        indent-tabs-mode nil)

and a small mapping.

(global-set-key (kbd "<f8>") 'ffap)

Minor modes

(want-drones rainbow-delimiters)

Company

(want-drone company)

(add-hook 'prog-mode-hook 'company-mode)
;;TODO BIND  :bind (:map company-mode-map
;; (("M-TAB" . company-complete-common)))
(with-eval-after-load 'company
  (diminish 'company-mode))

Editorconfig

(want-drone editorconfig)

(add-hook 'prog-mode-hook (editorconfig-mode 1))
(add-hook 'text-mode-hook (editorconfig-mode 1))
(with-eval-after-load 'editorconfig
  (diminish 'editorconfig-mode))

Evil Nerd Commenter

A good replacement for comment-dwim, but unline ~comment-dwim2~, it can’t alternate between commenting and commenting out (adding the comment delimiter at the start or the end of the line).

(want-drone evil-nerd-commenter)
(general-define-key "M-;"   'evilnc-comment-or-uncomment-lines
                    "C-M-;" 'evilnc-comment-or-uncomment-paragraphs
                    "C-c l" 'evilnc-quick-comment-or-uncomment-to-the-line
                    "C-c c" 'evilnc-copy-and-comment-lines
                    "C-c p" 'evilnc-comment-or-uncomment-paragraphs)

Flycheck

(want-drones flycheck
             flycheck-pos-tip pos-tip
             )

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

  (with-eval-after-load 'flycheck
    (diminish 'flycheck-mode))

Use popups instead of the modeline to display flycheck errors:

(with-eval-after-load 'flycheck
  (flycheck-pos-tip-mode))

Helm-dash

(want-drone helm-dash)

(setq helm-dash-docsets-path "~/.local/share/DashDocsets")

(add-hook 'c-mode-hook
          (lambda ()
            (setq-local helm-dash-docsets '("C"))

            (add-hook 'c++-mode-hook
                      (lambda ()
                        (setq-local helm-dash-docsets '("Boost" "C++" "Qt"))))

            (add-hook 'emacs-lisp-mode-hook
                      (lambda ()
                        (setq-local helm-dash-docsets '("Emacs Lisp"))))

            (add-hook 'haskell-mode-hook
                      (lambda ()
                        (setq-local helm-dash-docsets '("Haskell"))))

            (add-hook 'html-mode-hook
                      (lambda ()
                        (setq-local helm-dash-docsets '("HTML"))))

            (add-hook 'js-mode-hook
                      (lambda ()
                        (setq-local helm-dash-docsets '("JavaScript"))))

            (add-hook 'python-mode-hook
                      (lambda ()
                        (setq-local helm-dash-docsets '("Python 2" "Python 3"))))

            (add-hook 'rust-mode-hook
                      (lambda ()
                        (setq-local helm-dash-docsets '("Rust"))))))

(general-define-key :keymaps 'prog-mode-map
                  "<f1> <f1>" 'helm-dash-at-point)

Highlight-indent-guides

(want-drone highlight-indent-guides)

(setq highlight-indent-guides-method 'character
      highlight-indent-guides-character ?┃
      highlight-indent-guides-auto-character-face-perc 25)

(add-hook 'prog-mode-hook 'highlight-indent-guides-mode)

Outline and outshine

(want-drone outshine)

(add-hook 'prog-mode-hook 'outline-minor-mode)
(add-hook 'outline-minor-mode-hook 'outshine-hook-function)

We provide a function to easily create outline-heading-alist:

(defun thblt/mk-outline-heading-alist (before character after &optional start end)
  "Make an alist of (HEADING . LEVEL) usable as `outline-heading-alist.

For level n, BEFORE is concatenated with n times CHARACTER followed by AFTER.

Sequences start at START and end at END, default is 1--8."
  (unless start (setq start 1))
  (unless end (setq end 8))
  (mapcar (lambda (n) (cons (concat
                             before
                             (make-string n character)
                             after)
                            n))
          (number-sequence start end)))

Rainbow mode

Rainbow mode is similar to Atom’s Pigments plugin or something.

(want-drones kurecolor
             rainbow-mode)
(add-hook 'prog-mode-hook (rainbow-mode))
(add-hook 'css-mode-hook 'rainbow-mode)
(add-hook 'scss-mode-hook 'rainbow-mode)

(with-eval-after-load 'rainbow-mode
  (diminish 'rainbow-mode))

Programming languages

(want-drones lua-mode
             rust-mode)

C/C++

(want-drones clang-format
             company-irony
             company-irony-c-headers
             flycheck-irony
             irony)
(add-hook 'c-mode-common-hook 'irony-mode)
(add-hook 'irony-mode-hook 'irony-cdb-autosetup-compile-options)

(with-eval-after-load 'flycheck
  (add-hook 'flycheck-mode-hook #'flycheck-irony-setup))

(with-eval-after-load 'company
  (add-to-list 'company-backends 'company-irony))

(with-eval-after-load 'irony
  (diminish' irony-mode))
(add-hook 'c-mode-common-hook
          (lambda ()
            (local-set-key (kbd "C-c o") 'ff-find-other-file)))

Haskell

Intero mode is a “complete interactive development program for Haskell”:

(want-drones haskell-mode
             hayoo
             intero)

(add-hook 'haskell-mode-hook 'intero-mode-blacklist)
(general-define-key :keymaps 'haskell-mode-map
                    "<f1> <f1>" 'hayoo-query)

Lisps

(add-hook 'lisp-mode-hook
          (lambda ()
            (setq outline-heading-alist
                  (thblt/mk-outline-heading-alist ";;" ?\; " "))))

Web development

(want-drones emmet-mode
             haml-mode
             less-css-mode
             scss-mode
             skewer-mode
             web-mode)

(setq scss-compile-at-save nil)
(add-to-list 'auto-mode-alist '("\\.css\\'" . scss-mode))

(add-to-list 'auto-mode-alist '("\\.phtml\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.tpl\\.php\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.[agj]sp\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.as[cp]x\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.erb\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.mustache\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.djhtml\\'" . web-mode))

Misc syntaxes

(want-drones json-mode
             toml-mode
             yaml-mode

             cmake-mode)

Gettext (PO)

(want-drone po-mode)

(autoload 'po-mode "po-mode"
  "Major mode for translators to edit PO files" t)
(setq auto-mode-alist (cons '("\\.po\\'\\|\\.po\\." . po-mode)
                            auto-mode-alist))

Tools

This section deals with tools which don’t edit anything.

(want-drones debian-bug
             dired+)

Borg and their Queen

Borg

Borg is initialized from init.el. As with other Emacs’ package management systems, we still run the risk of keeping unneeded packages. What follows is an attempt to address this issue: a utility function (=want-drone) to declare that a package is required (declared in init.el), and a few more functions to keep track of what is installed using the dependency tree and the set of explicitly required packages as a base.

(require 'cl-lib)
(require 'epkg)

(defun thblt/borg-mk-dep-list ()
  ""
  (let ((drones (borg-drones)))
    (cl-pairlis drones
                (mapcar
                 (lambda (d)
                   (cl-remove-if-not
                    (lambda (p) (member p drones))
                    (mapcar 'car (epkg-required d))))
                 drones))))

(defun thblt/borg-clones-strict ()
  "Return a list of strict clones, ie clones that are not assimimated as submodules."
  (let ((drones (borg-drones)))
    (cl-remove-if (lambda (obj) (member obj drones)) (borg-clones))))

Automatic commit messages

(defun thblt/borg-git-electric-commit-message ()
  "Generate a commit message describing changes in Borg drones."
  (when (equal
         (file-truename default-directory)
         (file-truename user-emacs-directory))

    (cl-flet ((plural (verb count)
                      (if (zerop count)
                          ""
                        (format "%s %s%s"
                                verb
                                count
                                (if nosubject
                                    ""
                                  (progn
                                    (setq nosubject
                                          (if (= 1 count) " drone" " drones")))))))
              (relpath (path) (file-relative-name path borg-drone-directory)))

      (let* ((status
              (cl-remove-if-not (lambda (p)
                                  (string-prefix-p "lib/" (cadr p)))
                                (mapcar (lambda (s) (split-string s " " t))
                                        (magit-git-lines "status" "--porcelain"))))
             (assimilated (mapcar 'cadr (remove-if-not (lambda (i) (equal "A" (car i))) status)))
             (modified (mapcar 'cadr (remove-if-not (lambda (i) (equal "M" (car i))) status)))
             (removed (mapcar 'cadr (remove-if-not (lambda (i) (equal "D" (car i))) status)))
             (assimilated-c (length assimilated))
             (modified-c (length modified))
             (removed-c (length removed))
             (count (+ assimilated-c modified-c))
             (nosubject))

        (unless (zerop count)
          (concat
           (when (> count 1)
             (s-capitalize
              (concat (mapconcat 'identity
                                 `(
                                   ,(plural "remove" removed-c)
                                   ,(plural "assimilate" assimilated-c)
                                   ,(plural "upgrade" modified-c))
                                 ", ") "\n\n")))

           (mapconcat 'identity
                      `(
                        ,(mapconcat (lambda (d) (format "Remove %s" (relpath d)))
                                    removed "\n")

                        ,(mapconcat (lambda (d) (format "Assimilate %s" (relpath d)))
                                    assimilated "\n")

                        ,(mapconcat (lambda (d) (format "Upgrade %s to %s"
                                                        (relpath d)
                                                        (let ((default-directory (expand-file-name d default-directory)))
                                                          (car (magit-git-lines "describe" "--always" "--tags")))))
                                    modified "\n")) "\n")))))))

(with-eval-after-load 'magit
  (add-to-list 'thblt/git-electric-commit-message-functions 'thblt/borg-git-electric-commit-message))

Borg-Queen

(setq borg-queen-pgp-global-keys '("1B1336171A0B9064"))

Calendars

(want-drone calfw)

(setq cfw:display-calendar-holidays nil
      ;; Grid characters
      cfw:fchar-vertical-line ?│
      cfw:fchar-horizontal-line ?─
      cfw:fchar-junction ?┼
      cfw:fchar-top-junction ?┬
      cfw:fchar-top-left-corner ?╭
      cfw:fchar-top-right-corner ?╮
      cfw:fchar-left-junction ?├
      cfw:fchar-right-junction ?┤)

Dired

(persp-def-auto-persp "dired"
                      :mode-name "^dired.*"
                      :switch 'frame
                      )

(add-to-list 'thblt/context-starters '("dired" . (lambda () (call-interactively 'dired))))

Ebib

(want-drone ebib)

(setq ebib-bibtex-dialect 'biblatex)

ERC

(want-drone erc-hl-nicks)

(setq erc-server "irc.freenode.net"
      erc-port 7000
      erc-nick "thblt"
      erc-nick-uniquifier  "`"

      erc-server-auto-reconnect t

      erc-lurker-hide-list '("JOIN" "PART" "QUIT")
      erc-lurker-threshold-time 900 ; 15mn

      erc-header-line-format nil)

(add-hook 'erc-mode-hook (lambda ()
                           (visual-line-mode)
                           (erc-hl-nicks-mode)
                           (erc-fill-disable)))

Automatic perspective:

(persp-def-auto-persp "erc"
                      :mode-name "^erc.*"
                      :switch 'frame
                      )

(add-to-list 'thblt/context-starters '("erc" . erc-tls))

Magit and Git

(want-drones magit
             git-timemachine)

(general-define-key
 "C-x g s" 'magit-status
 "C-x g r" 'magit-list-repositories
 "C-x g t" 'git-timemachine)

Use Projectile projects as a source of repositories:

(defun thblt/update-magit-repository-directories (&rest _)
  (setq magit-repository-directories (mapcar (lambda (x) `(,x . 0)) projectile-known-projects)))

(advice-add 'magit-status :before 'thblt/update-magit-repository-directories)
(advice-add 'magit-list-repositories :before 'thblt/update-magit-repository-directories)

magit-list-repositories

magit-list-repositories provides a summary view of multiple repositories.

First, let’s configure the view.

(setq magit-repolist-columns
      '(
        ("Name"       25  magit-repolist-column-ident nil)
        ("Branch"     10  magit-repolist-column-branch)
        ("Version" 25  magit-repolist-column-version nil)
        ("Upstream"   15  magit-repolist-column-upstream)
        ("↓U"         5   magit-repolist-column-unpulled-from-upstream)
        ("↑U"         5   magit-repolist-column-unpushed-to-upstream)
        ("↓P"         5   magit-repolist-column-unpulled-from-pushremote)
        ("↑P"         5   magit-repolist-column-unpushed-to-pushremote)
        (""           6   magit-repolist-column-dirty)
        ("Path"       99  magit-repolist-column-path nil)))

An extra feature: update all remotes. Probably very dirty.

(require 'cl)
(require 'magit-repos)

(defun thblt/magit-repolist-refresh ()
  "@TODO Add documentation"
  (interactive)
  (goto-char (point-min))
  (catch 'done
    (while t
      (--if-let (tabulated-list-get-id)
          (progn
            (cd (expand-file-name it))
            (magit-fetch-all ())))

      (when (move-text--at-last-line-p)
        (throw 'done t))

      (forward-line)
      (redisplay))
    ()))

(define-key magit-repolist-mode-map (kbd "G") 'thblt/magit-repolist-refresh)

Electric commit messages

We create a small function hooked to git-commit-mode to automatically fill commit message when applicable. This function simply runs each function in a list, in turn, and insert the return value of the first one returning non-nil.

(defvar thblt/git-electric-commit-message-functions
  nil
  "A list of functions returning either nil or a commit message.
  These functions get called with `default-directory' set at the
  repository's.")

(defun thblt/git-electric-commit-message-hook ()
  "Run every function in ‘thblt/git-electric-commit-functions’
  and insert the return value of the first one returning non-nil.

This function is meant to be run as a hook in `git-commit-mode'."

  (let ((default-directory (vc-git-root default-directory)))
    (--when-let (cl-some 'funcall thblt/git-electric-commit-message-functions)
      (insert it))))

(autoload 'vc-git-root "vc-git")

Then plug everything together:

(with-eval-after-load 'git-commit
  (add-hook 'git-commit-mode-hook 'thblt/git-electric-commit-message-hook))

Mu4e

General

Configuration for mu4e is split between a published part, below, and a private part, tangled from ~/.emacs.d/thblt/mu4e.el. The public part contains common mu4e settings, the private parts defines accounts and bookmarks.

First, we may need to update the load-path. Official Debian build of Emacs don’t need that, but self-built versions do:

(eval-and-compile (let ((mu4epath "/usr/share/emacs/site-lisp/mu4e"))
                    (when (file-directory-p mu4epath)
                      (add-to-list 'load-path mu4epath))))

On NixOS, this is a bit more tricky. We need to find the mu binary, dereference it (since it will be a symlink), and find the path from this.

(eval-and-compile (let ((mu4epath
                          (concat
                           (file-name-directory
                            (file-truename
                             (executable-find "mu")))
                           "../share/emacs/site-lisp/mu4e")))
                     (when (and
                            (string-prefix-p "/nix/store/" mu4epath)
                            (file-directory-p mu4epath))
                       (message "Adding %s to load-path" (file-truename mu4epath))
                       (add-to-list 'load-path (file-truename mu4epath)))))

Each of my accounts is synced (by mbsync) to a folder at the root of the Maildir (eg, ~/.Mail/Academic/). We then need a function to switch contexts based on a regular expression on the current Maildir path. For some reason, this doesn’t come included with mu4e, so here it is, and it probably comes from here.

(defun mu4e-message-maildir-matches (msg rx)
  (when rx
    (if (listp rx)
        ;; if rx is a list, try each one for a match
        (or (mu4e-message-maildir-matches msg (car rx))
            (mu4e-message-maildir-matches msg (cdr rx)))
      ;; not a list, check rx
      (string-match rx (mu4e-message-field msg :maildir)))))

Then the bulk of the config:

(require 'mu4e-contrib)

(setq
 ;; Use ivy
 mu4e-completing-read-function 'ivy-completing-read

 ;; General settings
 message-send-mail-function 'smtpmail-send-it
 message-kill-buffer-on-exit t
 mu4e-change-filenames-when-moving t  ; Required for mbsync
 mu4e-get-mail-command "mbsync ovh"
 mu4e-headers-auto-update t
 mu4e-html2text-command 'mu4e-shr2text
 mu4e-maildir "~/.Mail/"
 mu4e-update-interval 60 ;; seconds
 mu4e-sent-messages-behavior 'sent

 ;; Behavior
 mu4e-compose-dont-reply-to-self t

 ;; UI settings
 mu4e-confirm-quit nil
 mu4e-hide-index-messages t
 mu4e-split-view 'vertical
 mu4e-headers-include-related t  ; Include related messages in threads
 mu4e-view-show-images t

 ;; UI symbols
 mu4e-use-fancy-chars t
 mu4e-headers-attach-mark '("" . "")
 mu4e-headers-encrypted-mark '("" . "")
 mu4e-headers-flagged-mark '("+" . "")
 mu4e-headers-list-mark '("" . "")
 mu4e-headers-new-mark '("" . "")
 mu4e-headers-read-mark '("" . "")
 mu4e-headers-replied-mark '("" . "")
 mu4e-headers-seen-mark '("" . "")
 mu4e-headers-unseen-mark '("" . "")
 mu4e-headers-unread-mark '("" . "")
 mu4e-headers-signed-mark '("" . "")
 mu4e-headers-trashed-mark '("T" . "T")

 mu4e-headers-from-or-to-prefix '("" . "")

 mu4e-headers-default-prefix '(" " . "")
 mu4e-headers-duplicate-prefix '("D" . "D")
 mu4e-headers-empty-parent-prefix '("X" . " X")
 mu4e-headers-first-child-prefix '("|" . "╰─")
 mu4e-headers-has-child-prefix '("+" . "╰┬")

 mu4e-headers-fields '(
                       (:flags          . 5)
                       (:mailing-list   . 18)
                       (:human-date     . 12)
                       (:from-or-to     . 25)
                       (:thread-subject . nil)
                       )

 mu4e-user-mail-address-list '(
                               "thblt@thb.lt"
                               "thibault.polge@malix.univ-paris1.fr"
                               "thibault.polge@univ-paris1.fr"
                               "thibault@thb.lt"
                               "tpolge@gmail.com"
                               )
 mu4e-context-policy 'pick-first
 mu4e-compose-context-policy 'pick-first)

(add-hook 'mu4e-view-mode-hook (lambda ()
                                 (setq visual-fill-column-width 80)
                                 (visual-line-mode 1)
                                 (visual-fill-column-mode 1)))

(general-define-key "<f12>"  'mu4e)
(general-define-key :keymaps 'mu4e-headers-mode-map
                    "("      'mu4e-headers-prev-unread
                    ")"      'mu4e-headers-next-unread)
(general-define-key :keymaps 'mu4e-view-mode-map
                    "("      'mu4e-view-headers-prev-unread
                    ")"      'mu4e-view-headers-next-unread
                    "c"      'visual-fill-column-mode)

Compose messages with org-mode tables and lists:

(add-hook 'message-mode-hook 'turn-on-orgtbl)
(add-hook 'message-mode-hook 'turn-on-orgstruct++)

Company

Enable company-mode completion in compose buffer until this issue gets fixed:

(add-hook 'message-mode-hook 'company-mode)

Notifications (mu4e-alert)

Enable notifications:

(want-drone mu4e-alert)

(with-eval-after-load 'mu4e
  (with-eval-after-load 'dotemacs-private
    (setq mu4e-alert-interesting-mail-query (concat "flag:unread AND " (mu4e-get-bookmark-query ?i)))
    ;;            (mu4e-alert-set-default-style 'libnotify)
    ;;            (mu4e-alert-enable-notifications)
    (mu4e-alert-enable-mode-line-display)))

Automatic perspective with persp-mode

(persp-def-auto-persp "mu4e"
                      :mode-name "^mu4e.*"
                      :switch 'frame
                      )

(add-to-list 'thblt/context-starters '("mu4e" . mu4e))

Password management (password-store)

(want-drones auth-password-store
             pass
             password-store)
(auth-pass-enable)

PDF Tools

(want-drone pdf-tools (tablist))

(setq pdf-info-epdfinfo-program (expand-file-name "server/epdfinfo" (borg-worktree "pdf-tools")))

(pdf-tools-install)

(with-eval-after-load 'tex
  (unless (assoc "PDF Tools" TeX-view-program-list-builtin)
    (add-to-list 'TeX-view-program-list-builtin
                 '("PDF Tools" TeX-pdf-tools-sync-view)))
  (add-to-list 'TeX-view-program-selection
               '(output-pdf "PDF Tools")))

(general-define-key :keymaps 'pdf-view-mode-map
                    "s a" 'pdf-view-auto-slice-minor-mode)

Regular expression builder

We use the string syntax, as advised on this Mastering Emacs’ article.

(setq reb-re-syntax 'string)

scpaste

Technomancy’s scpaste is a replacement for pastebin, paste.lisp.org, and similar services. It generates a HTML page out of a buffer or region and moves it over to a server using scp.

(setq scpaste-scp-destination "thblt@k9.thb.lt:/var/www/paste.thb.lt/"
      scpaste-http-destination "https://paste.thb.lt"
      scpaste-user-address "https://thb.lt"

      scpaste-make-name-function 'scpaste-make-name-from-timestamp)

Conclusion

HiDPI support (kindof)

This section is made of overrides to improve support for HiDPI monitors. It must be at the end, to avoid being overriden by default settings.

If we’re running on a HiDPI machine, we replace the flycheck fringe bitmap with a larger version.

(if (string-prefix-p  "maladict" system-name)
    (progn

      (set-face-attribute 'default nil
                          :height 070)

      (setq fringe-mode-explicit t)
      (set-fringe-mode '(16 . 0))

      (define-fringe-bitmap 'flycheck-fringe-bitmap-double-arrow
        (vector
         #b1000000000
         #b1100000000
         #b1110000000
         #b1111000000
         #b1111100000
         #b1111110000
         #b1111111000
         #b1111111100
         #b1111111110
         #b1111111111
         #b1111111111
         #b1111111110
         #b1111111100
         #b1111111000
         #b1111110000
         #b1111100000
         #b1111000000
         #b1110000000
         #b1100000000
         #b1000000000)
        20 10 'center)))

We should have started (or crashed) by now. It’s time to run the server!

(require 'server)
(unless (server-running-p) (server-start))

Load private configuration

Some parts of this configuration are private and stored elsewhere. We now need to load them. This file will provide a dotemacs-private feature, which is used elsewhere to defer configuration until some private bits are available.

(let ((mu4e-private-config (expand-file-name "dotemacs-private.org" user-emacs-directory)))

  (if (file-exists-p mu4e-private-config)
      (org-babel-load-file mu4e-private-config)
    (display-warning :warning "Private configuration missing")))

Report success

We finally set the initial contents of the scratch buffer. This makes it easy to notice when something went wrong (this may not be obvious in daemon mode)

(setq initial-scratch-message ";; ╔═╗┌─┐┬─┐┌─┐┌┬┐┌─┐┬ ┬\n;; ╚═╗│  ├┬┘├─┤ │ │  ├─┤\n;; ╚═╝└─┘┴└─┴ ┴ ┴ └─┘┴ ┴\n\n")

;; ╔═╗┌─┐┬─┐┌─┐┌┬┐┌─┐┬ ┬
;; ╚═╗│  ├┬┘├─┤ │ │  ├─┤
;; ╚═╝└─┘┴└─┴ ┴ ┴ └─┘┴ ┴