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.eloruse-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 pandC-p SPCbindings (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:
- Major modes dedicated to writing prose, as opposed to code or configuration.
- 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:
toc-orgprovides, guess what, automatic TOC generation for org-mode. This is better pinned to melpa-stable.
(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")
;; ╔═╗┌─┐┬─┐┌─┐┌┬┐┌─┐┬ ┬
;; ╚═╗│ ├┬┘├─┤ │ │ ├─┤
;; ╚═╝└─┘┴└─┴ ┴ ┴ └─┘┴ ┴