#+Title: Emacs Customizations #+Author: Ricardo Wurmus #+PROPERTY: header-args :tangle "~/.emacs.d/init.el" #+PROPERTY: mkdirp t #+OPTIONS: tasks:nil toc:1 * Introduction My Emacs configuration is a mess. As I’m writing this my Emacs configuration stretches across multiple files, each containing various snippets of code that seemed like a good idea to group. Unfortunately, there are a some things that don’t have a “natural” home. Enabling the same minor mode in various major modes is one of these cases—do I duplicate the hook and place it in a file for each major mode? Or do I write a new file for the minor mode in which I add it to the major modes at once? With multiple files I spend too much time trying to find the best place for any bit of configuration I add. This slows me down and sometimes I just append to the main =init.el=, so I often feel that my configuration is in need of reorganisation. But configuring Emacs should be fun! I don’t want it to create an uncomfortable clean-up task as a side-effect. This is why I’m now trying to use a literate approach with =org-mode=. My Emacs configuration should be prose first and code second. In my experience, finding the right spot in prose for a new paragraph requires a lot less effort as the text itself acts as a connection between unrelated bits of code. * How to use this? We take all code blocks in this file and assemble an =init.el= from it if the source file =init.org= is younger. At startup time we check if the =init.el= has to be regenerated. To get started you need to have an =init.el= with at least these contents. #+BEGIN_SRC elisp (setq lexical-binding t) (let ((orgfile (expand-file-name (concat user-emacs-directory "init.org"))) (target (expand-file-name (concat user-emacs-directory "init.el")))) (when (not (file-newer-than-file-p target orgfile)) (progn (require 'org) (org-babel-tangle-file orgfile) (byte-compile-file target) (load target)))) #+END_SRC * Performance Make startup faster by reducing the frequency of garbage collection and then use a hook to measure Emacs startup time. #+begin_src elisp ;; The default is 800 kilobytes. Measured in bytes. (setq gc-cons-threshold (* 50 1000 1000)) ;; Profile emacs startup (add-hook 'emacs-startup-hook (lambda () (message "*** Emacs loaded in %s with %d garbage collections." (format "%.2f seconds" (float-time (time-subtract after-init-time before-init-time))) gcs-done))) #+end_src Native compilation gives Emacs a speed boost, but it spews compiler warnings that are quite annoying. Silence them. #+begin_src elisp (setq comp-async-report-warnings-errors nil) #+end_src * Initialise packages Emacs is an operating system and I use it as such (see [[http://elephly.net/posts/2016-02-14-ilovefs-emacs.html][this blog post]]). I rely on quite a few extensions that have been made available on various ELPA repositories. Recently, I have moved to installing and managing Emacs packages like any other software package on my system with the functional package manager [[https://gnu.org/s/guix][GNU Guix]]. I find this more reliable, although at first it is slightly less convenient as I can no longer just use =package.el= but first need to package the Elisp code for Guix. I install a Guix profile just for Emacs using Guix Home. Make Emacs load packages from the profile: #+begin_src elisp (setq package-directory-list '("/home/rekado/.guix-home/emacs-profile/share/emacs/site-lisp")) #+end_src * Better defaults Emacs defaults are hostile to most people. They are what kept me from using Emacs for many years. I’m easily confused by the way the cursor (point) keeps jumping around when scrolling by pages. Let the cursor keep its screen position constant even when scrolling by full screens and don’t jump around when scrolling. #+BEGIN_SRC elisp (setq scroll-margin 7 scroll-step 1 scroll-conservatively 10000 scroll-preserve-screen-position 1) #+END_SRC Here are a few more simple tweaks: #+BEGIN_SRC elisp ;; No splash screen please ... (setq inhibit-startup-message t) ;; Use UTF-8 by default (prefer-coding-system 'utf-8-unix) ;; Keep all visited files in recentf (use-package recentf :init (setq recentf-max-saved-items nil)) ;; When point is on a file name and find-file is used, populate the prompt with the name of the file at point. (ffap-bindings) ;; display tool tips in echo area only (tooltip-mode -1) (tool-bar-mode -1) (menu-bar-mode -1) (scroll-bar-mode -1) ;; by default Emacs will only resize the frame line by line (setq frame-resize-pixelwise t) ;; don’t force me to input “yes” or “no” (defalias 'yes-or-no-p 'y-or-n-p) ;; Use ibuffer instead of list-buffer (global-set-key (kbd "C-x C-b") 'ibuffer) ;; enable mouse scrolling (mouse-wheel-mode 1) ;(set-frame-parameter nil 'undecorated t) #+END_SRC I don’t use Helm because it’s too “busy” but I do want a more intelligent way to select buffers and find files. #+BEGIN_SRC elisp (require 'use-package) (use-package vertico :init (vertico-mode)) #+END_SRC Save recent completion selections. #+BEGIN_SRC elisp (use-package savehist :init (savehist-mode)) #+END_SRC Display more information for vertico choices. #+BEGIN_SRC elisp (use-package marginalia :after vertico :init (marginalia-mode)) #+END_SRC Match any part in any order. #+BEGIN_SRC elisp (use-package orderless :custom (completion-styles '(orderless basic)) (completion-category-overrides '((file (styles basic partial-completion))))) #+END_SRC Also tell Emacs that I want to have a separate file for all other customisations that are handled through =M-x customize=. #+BEGIN_SRC elisp ;; Keep emacs Custom-settings in separate file (setq custom-file (expand-file-name "custom.el" user-emacs-directory)) (load custom-file) #+END_SRC * Theme The default behavior of Emacs is that you can compose multiple themes; however, in practice that’s never done and will likely just mess things up. With this little advice, we tell Emacs that once a theme is loaded, all prior themes should be disabled. #+BEGIN_SRC elisp (defadvice load-theme (before theme-dont-propagate activate) (progn (mapc #'disable-theme custom-enabled-themes) (run-hooks 'after-load-theme-hook))) #+END_SRC The Modus themes look pretty nice. #+BEGIN_SRC elisp (use-package modus-themes :config (setq modus-themes-completions '(opinionated)) (setq modus-themes-headings '((agenda-structure . (variable-pitch light 1.5)) (0 . (variable-pitch regular 2.0)) (1 . (variable-pitch background light overline 1.5)) (2 . (variable-pitch overline rainbow 1.3)) (3 . (variable-pitch overline 1.1)) (t . (monochrome)))) (setq modus-themes-bold-constructs t) (setq modus-themes-org-blocks t) (setq modus-themes-variable-pitch-ui nil) (setq modus-themes-prompts '(bold)) (setq modus-themes-to-toggle '(modus-operandi-tinted modus-vivendi-tinted)) (setq modus-themes-common-palette-overrides (append '((border-mode-line-active unspecified) (border-mode-line-inactive unspecified) (window-divider fg-main) ;; Parenthesis matching (bg-paren-match bg-magenta-intense) ;; Org Agenda (date-deadline magenta-warmer) (date-scheduled green-cooler) (date-weekday fg-main) (date-event fg-dim) (date-now blue) (prose-done fg-alt) (prose-todo yellow) ;; Region highlight (bg-region bg-yellow-subtle) (fg-region unspecified) ;; Fewer colors in email (mail-cite-0 fg-dim) (mail-cite-1 blue-faint) (mail-cite-2 fg-dim) (mail-cite-3 blue-faint) (mail-part cyan-warmer) (mail-recipient blue-warmer) (mail-subject magenta-cooler) (mail-other cyan-warmer)) modus-themes-preset-overrides-faint)) (load-theme 'modus-operandi-tinted :no-confirm) ;; Add frame borders and window dividers (modify-all-frames-parameters '((right-divider-width . 1) (internal-border-width . 20))) (set-face-background 'fringe (face-attribute 'default :background))) #+END_SRC Dired mode becomes much prettier with =nerd-icons=. #+BEGIN_SRC elisp (use-package nerd-icons) #+END_SRC Customize the mode line: #+BEGIN_SRC elisp (use-package mini-echo :config (mini-echo-mode) :init ;; set default segments of long/short style (setq mini-echo-default-segments '(:long ("major-mode" "buffer-name" "vcs" "buffer-position" "flymake" "process" "selection-info" "narrow" "macro" "profiler") :short ("buffer-name-short" "buffer-position" "process" "profiler" "selection-info" "narrow" "macro")))) #+END_SRC Dim windows that are not active. #+begin_src elisp (use-package dimmer :ensure :defer :init (setq dimmer-watch-frame-focus-events nil) (setq dimmer-use-colorspace :rgb) (setq dimmer-fraction 0.1) (setq dimmer-adjustment-mode :foreground) (defun advise-dimmer-config-change-handler () "Advise to only force process if no predicate is truthy." (let ((ignore (cl-some (lambda (f) (and (fboundp f) (funcall f))) dimmer-prevent-dimming-predicates))) (unless ignore (when (fboundp 'dimmer-process-all) (dimmer-process-all t))))) (defun corfu-frame-p () "Check if the buffer is a corfu frame buffer." (string-match-p "\\` \\*corfu" (buffer-name))) (defun dimmer-configure-corfu () "Convenience settings for corfu users." (add-to-list 'dimmer-prevent-dimming-predicates #'corfu-frame-p)) (defun update-dimmer-style () (let* ((theme (modus-themes--current-theme)) (what (pcase theme ('modus-operandi 'light) ('modus-operandi-tinted 'light) (_ 'dark)))) (if (eq what 'dark) (progn (setq dimmer-fraction 0.3) (setq dimmer-adjustment-mode :foreground)) (progn (setq dimmer-fraction 0.05) (setq dimmer-adjustment-mode :background))))) (add-hook 'modus-themes-after-load-theme-hook #'update-dimmer-style) :config (advice-add 'dimmer-config-change-handler :override 'advise-dimmer-config-change-handler) (dimmer-configure-corfu) (dimmer-mode t)) #+end_src * Default fonts I like pretty faces. For coding I like to use the DejaVu Sans Mono font. In =org-mode= and in =eww= I like to use a font with variable pitch instead of the default mono-spaced font. #+BEGIN_SRC elisp (set-face-attribute 'fixed-pitch nil :family "DejaVu Sans Mono" :height 130) (set-face-attribute 'variable-pitch nil :family "Vollkorn" :height 150) (set-face-attribute 'default nil :family "DejaVu Sans Mono" :height 130) #+END_SRC * Guix Store paths have long hashes. In most cases I don’t really care, so I use =guix-prettify-mode= to hide them. #+BEGIN_SRC elisp (when (require 'guix-prettify nil t) (global-guix-prettify-mode)) #+END_SRC I’m often building Guix packages in the shell. =guix-build-log-minor-mode= gives me key bindings to fold and jump over build phases, and it adds pretty faces to the otherwise bland wall of text. #+BEGIN_SRC elisp (add-hook 'shell-mode-hook 'guix-build-log-minor-mode) #+END_SRC I’m monitoring the Guix build farm =berlin.guixsd.org=, which is hosted at the MDC. #+BEGIN_SRC elisp (setq guix-hydra-url "https://ci.guix.gnu.org") #+END_SRC For bug and patch tracking the Guix project uses debbugs. Here are some better defaults for using =debbugs-gnu= with Guix: #+BEGIN_SRC elisp (eval-when-compile (require 'debbugs-gnu)) (with-eval-after-load "debbugs-gnu" (setq debbugs-gnu-default-packages '("guix" "guix-patches")) (add-to-list 'debbugs-gnu-all-packages "guix-patches")) #+END_SRC Oleg Pykhalov shared this useful snippet to list bugs for which I am listed as the owner. #+BEGIN_SRC elisp (with-eval-after-load "debbugs-gnu" (defun my/debbugs-gnu () (interactive) (let ((debbugs-gnu-current-query `((submitter . ,user-mail-address)))) (debbugs-gnu nil)))) #+END_SRC When working on Guix it helps to reduce boilerplate with snippets. I like to have YASnippet enabled and let it use the snippets that are provided with Guix: #+BEGIN_SRC elisp (use-package yasnippet :config (add-to-list 'yas-snippet-dirs "~/dev/gx/branches/master/etc/snippets") (yas-global-mode)) #+END_SRC * Org-mode This is my org mode configuration. Much of it is in one big blob and I haven’t yet taken the time to document it. #+BEGIN_SRC elisp (setq org-ellipsis "⤵") (setq org-src-fontify-natively t) (global-set-key (kbd "C-c o l") 'org-store-link) (global-set-key (kbd "C-c o a") 'org-agenda) ;; TODO: make these available in org-mode only (global-set-key (kbd "C-c o s") 'org-schedule) (setq org-log-done t) (setq org-return-follows-link t) (setq org-directory "~/Documents/org") (setq org-agenda-files (mapcar (lambda (x) (concat org-directory x)) (list "/master.org" "/email.org" "/todo.org" "/inbox.org" "/birthdays.org"))) (setq org-default-notes-file (concat org-directory "/notes.org")) (setq org-agenda-custom-commands '(("w" todo "WAITING" nil) ("n" todo "NEXT" nil) ("d" "Agenda + Next Actions" ((agenda) (todo "NEXT"))))) (setq org-fontify-done-headline t) (defun gtd () (interactive) (find-file (concat org-directory "/master.org"))) (use-package org-modern :after org :init (set-face-background 'org-block-begin-line (face-attribute 'default :background)) (setq ;; Edit settings org-auto-align-tags nil org-tags-column 0 org-fold-catch-invisible-edits 'show-and-error org-special-ctrl-a/e t org-insert-heading-respect-content t ;; Org styling, hide markup etc. org-hide-emphasis-markers t org-pretty-entities t org-ellipsis "…" ;; Agenda styling org-agenda-tags-column 0 org-agenda-block-separator ?─ org-agenda-time-grid '((daily today require-timed) (800 1000 1200 1400 1600 1800 2000) " ┄┄┄┄┄ " "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄") org-agenda-current-time-string "⭠ now ─────────────────────────────────────────────────") (global-org-modern-mode)) #+END_SRC I can never remember the syntax for babel blocks. With =org-tempo= I can type = (length pieces) 1) (concat (car pieces) ":/") "/")))) (defun my/tramp-home () "Print home directory path on the remote host." (interactive) (let ((pieces (split-string (eshell/pwd) ":/"))) (insert (if (> (length pieces) 1) (concat (car pieces) ":~/") "~/")))) :bind (:map eshell-mode-map (("C-c /" . my/tramp-root) ("C-c ~" . my/tramp-home)))) #+END_SRC Enable =coterm-mode= globally. #+begin_src elisp (use-package coterm :init (coterm-mode)) #+end_src TODO: here’s the rest of my shell configuration: #+BEGIN_SRC elisp (require 'shell-switcher) (setq shell-switcher-mode t) (add-hook 'eshell-mode-hook 'shell-switcher-manually-register-shell) (add-hook 'shell-mode-hook 'shell-switcher-manually-register-shell) (setq shell-switcher-new-shell-function 'shell-switcher-make-shell) ;; use cat as the pager in shell mode, because shell-mode is not an ;; ANSI terminal (setenv "PAGER" "cat") ;; C-d on an empty line in the shell terminates the process. (defun my/comint-delchar-or-eof-or-kill-buffer (arg) (interactive "p") (if (null (get-buffer-process (current-buffer))) (kill-buffer) (comint-delchar-or-maybe-eof arg))) (add-hook 'shell-mode-hook (lambda () ;; needed for proper display of "ls" (setq tab-width 8) ;; load shared bash history (setq comint-input-ring-file-name "~/.bash_history") (comint-read-input-ring t) (define-key shell-mode-map (kbd "C-d") 'my/comint-delchar-or-eof-or-kill-buffer) (define-key shell-mode-map (kbd "") 'comint-previous-matching-input-from-input))) ;; Show current path instead of just "*shell*<2>" (setq uniquify-buffer-name-style 'forward) (setq uniquify-min-dir-content 1024) (require 'uniquify) #+END_SRC * Magit #+BEGIN_SRC elisp (use-package magit :config ;; full screen magit-status (defadvice magit-status (around magit-fullscreen activate) (window-configuration-to-register :magit-fullscreen) ad-do-it (delete-other-windows)) (defun my/magit-quit-session () "Restores the previous window configuration and kills the magit buffer" (interactive) (kill-buffer) (jump-to-register :magit-fullscreen)) :custom (magit-diff-refine-hunk 'all) :bind (("C-c m" . magit-status) :map magit-status-mode-map ("q" . my/magit-quit-session))) #+END_SRC * Completion Corfu provides automatic completion. I like to enable it in all modes, because it also integrates with dabbrev. #+BEGIN_SRC elisp (use-package corfu :init (global-corfu-mode) :config (defun corfu-enable-in-minibuffer () "Enable Corfu in the minibuffer if `completion-at-point' is bound." (when (where-is-internal #'completion-at-point (list (current-local-map))) (setq-local corfu-echo-delay nil ;Disable automatic echo and popup corfu-popupinfo-delay nil) (corfu-mode 1))) (add-hook 'minibuffer-setup-hook #'corfu-enable-in-minibuffer)) ;; Use Dabbrev with Corfu! (use-package dabbrev ;; Swap M-/ and C-M-/ :bind (("M-/" . dabbrev-completion) ("C-M-/" . dabbrev-expand)) ;; Other useful Dabbrev configurations. :custom (dabbrev-ignored-buffer-regexps '("\\.\\(?:pdf\\|jpe?g\\|png\\)\\'"))) #+END_SRC Hippie expand is a neat way to expand text based on already existing text. Unfortunately, it collides with paredit (or smartparens) in that it may insert expansions that include unmatched parentheses. To avoid this I disable two types of expansions: #+BEGIN_SRC elisp (dolist (f '(try-expand-line try-expand-list)) (setq hippie-expand-try-functions-list (remq f hippie-expand-try-functions-list))) #+END_SRC I also use snippets for commonly typed expressions. #+BEGIN_SRC elisp (use-package yasnippet :init (yas-global-mode 1)) #+END_SRC * Paste The =scpaste= package allows me to quickly paste text to my personal web site. It only needs to know where to place the text files and where they would be publicly accessible via HTTP. #+BEGIN_SRC elisp (use-package scpaste :init (setq scpaste-http-destination "https://elephly.net/paste") (setq scpaste-scp-destination "elephly.net:~/elephly.net/paste/") (setq scpaste-scp-port "1022") (setq scpaste-make-name-function #'scpaste-make-name-from-timestamp)) #+END_SRC * Pretty symbols #+BEGIN_SRC elisp (defun my/pretty-js-symbols () (push '("===" . ?≡) prettify-symbols-alist) (push '("function" . ?𝑓) prettify-symbols-alist)) (defun my/pretty-r-symbols () (push '("%>%" . ?⤚) prettify-symbols-alist) (push '("%$%" . ?⤜) prettify-symbols-alist) (push '("==" . ?≡) prettify-symbols-alist) (push '("function" . ?𝑓) prettify-symbols-alist)) (when (boundp 'global-prettify-symbols-mode) (add-hook 'js2-mode-hook 'my/pretty-js-symbols) (add-hook 'ess-mode-hook 'my/pretty-r-symbols) (add-hook 'inferior-ess-mode-hook 'my/pretty-r-symbols) (global-prettify-symbols-mode +1)) #+END_SRC * Resize buffer margins dynamically When writing Org-mode documents or when browsing the web with Eww I prefer to see shorter lines. =olivetti-mode= adjusts the buffer margins such that the buffer contents are restricted in width and centered. I find this much more readable when editing Org documents or browsing with Eww. #+BEGIN_SRC elisp (use-package olivetti :hook ((org-mode . olivetti-mode) (markdown-mode . olivetti-mode))) #+END_SRC * Music I run MPC on a local server. #+BEGIN_SRC elisp (use-package simple-mpc :init (setq simple-mpc-arguments "-h 192.168.178.20")) #+END_SRC * Lilypond Activate Lilypond mode when I’m opening a Lilypond score. #+BEGIN_SRC elisp (require 'lilypond-mode) (add-to-list 'auto-mode-alist '("\\.ly\\'" . LilyPond-mode)) #+END_SRC Enable =subword-mode= in Lilypond files because I use CamelCase for music variables. #+BEGIN_SRC elisp (add-hook 'LilyPond-mode-hook 'subword-mode) #+END_SRC I like to render Lilypond snippets in Org mode buffers. To do that I need to load the Lilypond backend first. However, I don’t think this should be enabled by default. #+BEGIN_SRC elisp :tangle nil (with-eval-after-load "org" (require 'ob-lilypond)) #+END_SRC * Scheme development Geiser makes Scheme development really nice. It’s also used for Guix development in combination with =guix-devel-mode=, so I’m adding the Guix development directory to Guile’s load path in all Geiser sessions. #+BEGIN_SRC elisp (with-eval-after-load "geiser" (setq geiser-active-implementations '(guile)) (setq geiser-guile-load-path '("~/dev/gx/branches/master"))) #+END_SRC Automatically start =guix-devel-mode= when in =scheme-mode= because I’m likely working on Guix anyway. #+BEGIN_SRC elisp (add-hook 'scheme-mode-hook 'guix-devel-mode) #+END_SRC Parentheses don’t annoy me but I still prefer to have them fade into the background a little. This is what =paren-face-mode= does. #+BEGIN_SRC elisp (require 'paren-face) (global-paren-face-mode 1) #+END_SRC Emacs also highlights matching parentheses, but it does so with a delay. Here I’m disabling the delay. #+BEGIN_SRC elisp (require 'paren) (setq show-paren-delay 0) (show-paren-mode 1) #+END_SRC Editing lispy languages is no fun without =paredit=, a mode to enforce balanced parentheses. Enable it automatically when editing Scheme, Common Lisp, or Elisp. #+BEGIN_SRC elisp (add-hook 'scheme-mode-hook (lambda () (paredit-mode 1))) (add-hook 'emacs-lisp-mode-hook (lambda () (paredit-mode 1))) (add-hook 'lisp-mode-hook (lambda () (paredit-mode 1))) (add-hook 'geiser-repl-mode-hook (lambda () (paredit-mode 1))) #+END_SRC Also enable =paredit= when editing Elisp in the minibuffer. #+BEGIN_SRC elisp ;; Enable `paredit-mode' in the minibuffer, during `eval-expression'. (defun conditionally-enable-paredit-mode () (if (eq this-command 'eval-expression) (paredit-mode 1))) (add-hook 'minibuffer-setup-hook 'conditionally-enable-paredit-mode) #+END_SRC Some customisations for =paredit=. #+BEGIN_SRC elisp (eval-when-compile (require 'paredit)) (with-eval-after-load "paredit" ;; don't hijack \ please (define-key paredit-mode-map (kbd "\\") nil) ;; keybindings (define-key paredit-mode-map (kbd "M-C-") 'backward-kill-sexp) ;; making paredit work with delete-selection-mode (put 'paredit-forward-delete 'delete-selection 'supersede) (put 'paredit-backward-delete 'delete-selection 'supersede) (put 'paredit-newline 'delete-selection t)) #+END_SRC TODO: the parentheses adjustments should happen only when I’m in programming mode. * Window management I agree with this assessment of Emacs window splitting behavior in the README of the =perspective= package: #+BEGIN_QUOTE Emacs has bad default behavior when it comes to window handling: many commands and modes have a habit of splitting existing windows and changing the user's carefully thought-out window layout. This tends to be a more serious problem for people who run Emacs on large displays (possibly in full-screen mode): the greater amount of screen real estate makes it easy to split the frame into many smaller windows, making any unexpected alterations more disruptive. #+END_QUOTE We configure =display-buffer-alist= to reuse certain locations for buffers like documentation, compilation, etc. #+BEGIN_SRC elisp (setq display-buffer-alist '(;; top side window ("\\*\\(Flycheck\\|Flymake\\|Package-Lint\\|vc-git :\\).*" (display-buffer-in-side-window) (window-height . 0.16) (side . top) (slot . 0) (window-parameters . ((no-other-window . t) (mode-line-format . (" " mode-line-buffer-identification))))) ("\\*\\(Backtrace\\|Warnings\\|Compile-Log\\|Messages\\)\\*" (display-buffer-in-side-window) (window-height . 0.16) (side . top) (slot . 1) (window-parameters . ((no-other-window . t) (mode-line-format . (" " mode-line-buffer-identification))))) ;; bottom side window (".*\\*Completions.*" (display-buffer-in-side-window) (window-height . 0.16) (side . bottom) (slot . 0) (window-parameters . ((no-other-window . t)))) ("\\*Org Agenda\\*" (display-buffer-in-direction) (direction . leftmost) (window-width . 0.40) (window-parameters . ((mode-line-format . nil)))) ("\\*\\(Help\\|Faces\\).*" (display-buffer-in-side-window) (side . left) (slot . 0) (window-width . 0.35) (window-parameters . ((no-other-window . nil) (mode-line-format . nil)))) ;; bottom buffer (NOT side window) ("\\*\\vc-\\(incoming\\|outgoing\\).*" (display-buffer-at-bottom)) ;(".*" (reusable-frames . t)) )) (setq window-combination-resize t) (setq even-window-sizes 'height-only) (setq display-buffer-reuse-frames t) ; reuse windows in other frames ;; If non-nil, left and right side windows occupy full frame height. ;; If nil, top and bottom side windows occupy full frame width. (setq window-sides-vertical nil) #+END_SRC The Help window might be a little too narrow, so we use a hook to switch to =visual-line-mode=: #+begin_src elisp (add-hook 'help-mode-hook (lambda () (visual-line-mode 1))) #+end_src Side windows cannot be navigated, so we need another mechanism to close them all. #+begin_src elisp (global-set-key (kbd "C-x x") 'window-toggle-side-windows) #+end_src * Email TODO: this is a big blob of email configuration. Document this properly! #+begin_src elisp (defun my/set-mu4e-bookmarks (maildir) (let ((guix "(list:guix-devel.gnu.org OR list:bug-guix.gnu.org OR list:help-guix.gnu.org OR list:guix-sysadmin.gnu.org OR list:guix-security.gnu.org OR list:guix-patches.gnu.org OR list:guix-commits.gnu.org)") (guile "(list:guile-user.gnu.org OR list:guile-devel.gnu.org OR list:bug-guile.gnu.org)") (hurd "(list:bug-hurd.gnu.org)") (kita "(from:hvd-bb.de OR tag:kita)") (waldow "(list:waldow.googlegroups.com OR from:artvivendi-immobilien.de)") (unread "(flag:unread AND NOT flag:trashed)") (me "(body:rekado OR body:Ricardo)")) (setq mu4e-bookmarks (list ;; TODO: don't match my own signature (list (concat me " " unread) "Mentioning me (unread)" ?R) (list (concat "maildir:\"/" maildir "/INBOX\"") "Inbox" ?i) (list "date:today..now" "Today's messages" ?t) (list "date:today..now" "Last 7 days" ?w) (list (concat "maildir:\"/" maildir "/INBOX\"" " " unread) "Unread messages" ?u) (list (concat guix " " unread) "Guix" ?1) (list (concat guile " " unread) "Guile" ?2) (list (concat hurd " " unread) "Hurd" ?3) (list waldow "Waldow" ?4) (list (concat kita " " unread) "Kita" ?5) (list (concat "maildir:\"/private/mailinglists\"" " " unread " NOT " guix " NOT " guile " NOT " hurd " NOT " waldow " NOT " kita) "Unread list messages" ?m) (list (concat "maildir:\"/" maildir "/INBOX\" flag:flagged") "Flagged" ?f))))) #+end_src #+begin_src elisp (defun my/maybe-reply-encrypted () "Encrypt automatically if parent message was also encrypted." (let ((msg mu4e-compose-parent-message)) (when (and msg (or (member 'encrypted (mu4e-message-field msg :flags)) (string-match "-----BEGIN PGP MESSAGE-----$" (mu4e-message-field msg :body-txt)))) (mml-secure-message-sign-encrypt)))) #+end_src #+begin_src elisp (defun my/mu4e-add-headers () "Add some personal headers." (save-excursion (message-add-header "X-URL: https://elephly.net\n") (message-add-header "X-PGP-Key: https://elephly.net/rekado.pubkey\n") (message-add-header "X-PGP-Fingerprint: BCA6 89B6 3655 3801 C3C6 2150 197A 5888 235F ACAC"))) #+end_src #+begin_src elisp (use-package mu4e :bind ("" . mu4e) :after (epa message) :config (setq my/mu4e-context-private (make-mu4e-context :name "Private" :enter-func (lambda () (mu4e-message "Switch to the Private context") (my/set-mu4e-bookmarks "private")) :match-func (lambda (msg) (when msg (or (mu4e-message-contact-field-matches msg :to (rot13 "erxnqb@ryrcuyl.arg")) (mu4e-message-contact-field-matches msg :from (rot13 "erxnqb@ryrcuyl.arg")) ;; Additional check if this is a mailing list email. (and (mu4e-message-field msg :mailing-list) (zerop (call-process "grep" nil nil nil "-E" "^Delivered-To: .*elephly.net" (mu4e-message-field msg :path))))))) :vars `((user-mail-address . ,(rot13 "erxnqb@ryrcuyl.arg")) (user-full-name . ,(rot13 "Evpneqb Jhezhf")) (mu4e-sent-folder . "/private/Sent") (mu4e-trash-folder . "/private/Trash") (mu4e-refile-folder . "/private/Archives") (mu4e-drafts-folder . "/private/Drafts") (mu4e-compose-signature . "Ricardo")))) (setq my/mu4e-context-work (make-mu4e-context :name "Work" :enter-func (lambda () (mu4e-message "Switch to the Work context") (my/set-mu4e-bookmarks "mdc-personal")) :match-func (lambda (msg) (when msg (or (mu4e-message-contact-field-matches msg :to (rot13 "evpneqb.jhezhf@zqp-oreyva.qr")) (mu4e-message-contact-field-matches msg :from (rot13 "evpneqb.jhezhf@zqp-oreyva.qr")) ;; Additional check if this is a mailing list email. (and (mu4e-message-field msg :mailing-list) (zerop (call-process "grep" nil nil nil "-E" "^Received: from .*mdc-berlin.de" (mu4e-message-field msg :path))))))) :vars `((user-mail-address . ,(rot13 "evpneqb.jhezhf@zqp-oreyva.qr")) (user-full-name . ,(rot13 "Evpneqb Jhezhf")) (mu4e-sent-folder . "/mdc-personal/Sent Items") (mu4e-trash-folder . "/mdc-personal/Deleted Items") (mu4e-refile-folder . "/mdc-personal/Archive") (mu4e-drafts-folder . "/mdc-personal/Drafts") (mu4e-compose-signature . ,(rot13 (concat "Evpneqb Jhezhf\n\n" "Flfgrz nqzvavfgengbe\n" "OVZFO - Fpvragvsvp Ovbvasbezngvpf Cyngsbez\n" "Znk Qryoehrpx Pragre sbe Zbyrphyne Zrqvpvar\n\n" "rznvy: evpneqb.jhezhf@zqp-oreyva.qr\n" (concat "gry: +49 30 9406 " (number-to-string (+ (* 1 2 2 3 4 5 6) (expt 2 8) 100))))))))) (setq mu4e-contexts (list my/mu4e-context-private my/mu4e-context-work)) (setq mu4e-get-mail-command "/home/rekado/.guix-home/profile/bin/mbsync -c /home/rekado/.config/mbsync.conf -a") ;; Rename files when moving. This is NEEDED when using mbsync or ;; else there is a problem of duplicate UIDs! (setq mu4e-change-filenames-when-moving t) (setq mu4e-attachment-dir "~/Downloads") (setq mu4e-completing-read-function 'completing-read) (setq mu4e-compose-dont-reply-to-self t) (setq mu4e-compose-signature-auto-include t) ;; Don't update list of emails automatically. I update the list ;; manually with "g". (setq mu4e-headers-auto-update nil) (setq mu4e-headers-fields '((:human-date . 12) (:from . 22) (:subject . nil))) (setq mu4e-search-include-related t) (setq mu4e-headers-seen-mark '("" . "")) (setq mu4e-headers-unread-mark '("u" . "✉")) (setq mu4e-hide-index-messages t) (setq mu4e-update-interval nil) (setq mu4e-use-fancy-chars t) ;; Don't ask to quit (setq mu4e-confirm-quit nil) ;; Don't ask for a 'context' upon opening mu4e (setq mu4e-context-policy 'pick-first) ;; set up email sending with msmtp (setq mail-user-agent 'mu4e-user-agent) (setq mail-specify-envelope-from t) (setq mail-envelope-from 'header) ;; use msmtp instead of sendmail (setq sendmail-program "~/.guix-home/profile/bin/msmtp") ;; Crypto (setq mml-secure-openpgp-encrypt-to-self t) (setq mml-secure-openpgp-sign-with-sender t) ;; Don't open a new window to show the results of signature validation (setq epa-popup-info-window nil) (setq shr-color-visible-luminance-min 40) ;; use imagemagick, if available (when (fboundp 'imagemagick-register-types) (imagemagick-register-types)) :hook ((mu4e-compose-mode . #'my/mu4e-add-headers) (mu4e-compose-mode . #'my/maybe-reply-encrypted))) #+end_src Enable buttons to reply to icalendar invitations: #+begin_src elisp (use-package mu4e-icalendar :after mu4e :init (mu4e-icalendar-setup)) #+end_src Integrate mu4e with Org mode. #+begin_src elisp (use-package org-mu4e :after (org-mode mu4e)) #+end_src #+begin_src elisp (use-package mu4e-actions :after mu4e :init (add-to-list 'mu4e-view-actions '("git am" . mu4e-action-git-apply-mbox)) (add-to-list 'mu4e-headers-actions '("git am" . mu4e-action-git-apply-mbox)) (add-to-list 'mu4e-view-actions '("retag message" . mu4e-action-retag-message) t)) #+end_src #+begin_src elisp (use-package message :custom (message-kill-buffer-on-exit t) (message-sendmail-envelope-from 'header) (message-send-mail-function 'message-send-mail-with-sendmail)) ;; TODO: move this elsewhere (use-package typo :hook ((message-mode org-mode erc-mode) . typo-mode)) #+end_src Add action to delete attachments (https://emacs.stackexchange.com/a/70992) #+begin_src elisp (defun my-mime-part-filename (num) "Filename of MIME part numbered num in gnus-article-mode." ;; Check whether the specified part exists. (when (> num (length gnus-article-mime-handle-alist)) (error "No such part")) ;; Move point to MIME part (when (gnus-article-goto-part num) ;; Get handle for MIME part at point (let ((handle (get-text-property (point) 'gnus-data))) (when handle ;; Return file name of handle (mm-handle-filename handle))))) (defun my-delete-attachment (num) "Remove email attachment from mu4e using altermime." (let* ((path (mu4e-message-field (mu4e-message-at-point) :path)) (filename (my-mime-part-filename num)) (cmd (format "altermime --input='%s' --remove='%s'" path filename))) (when (and filename (yes-or-no-p (format "Remove '%s'?" filename))) (shell-command cmd) (mu4e-message cmd)))) (defun my-delete-all-attachments (msg) "Remove all email attachments in mu4e using altermime." (let* ((path (mu4e-message-field msg :path)) (subject (mu4e-message-field msg :subject)) (cmd (format "altermime --input='%s' --removeall" path))) (when (yes-or-no-p (format "Remove all attachments from '%s'?" subject)) (shell-command cmd) (mu4e-message cmd)))) (add-to-list 'mu4e-view-mime-part-actions '(:name "delete-attachment" :handler my-delete-attachment :receives index)) (add-to-list 'mu4e-headers-actions '("Delete-all-attachments" . my-delete-all-attachments)) #+end_src Distinguish the different fields in the mu4e headers view: #+begin_src elisp (use-package mu4e-column-faces :after mu4e :config (mu4e-column-faces-mode)) #+end_src * TODO More stuff This is even more stuff to be done after initialising packages. I still need to process all of this and clean it up. #+BEGIN_SRC elisp (require 'projectile) (projectile-mode) #+END_SRC * TODO And even more #+BEGIN_SRC elisp (setq backup-directory-alist `(;; Do not backup or auto-save remote files to prevent delays. (,tramp-file-name-regexp . nil) ;; Write backup files to a dedicated directory. ("." . ,(expand-file-name (concat user-emacs-directory "backups"))))) ;; Make backups of files, even when they're in version control (setq vc-make-backup-files t) (add-to-list 'auto-mode-alist '("\\.markdown\\'" . markdown-mode)) (add-to-list 'auto-mode-alist '("\\.md\\'" . markdown-mode)) (add-to-list 'auto-mode-alist '("\\.js$" . js2-mode)) (setq scss-compile-at-save nil) (desktop-save-mode t) (setq desktop-restore-eager 5) ; restore buffers lazily (setq desktop-lazy-idle-delay 3) ; no hurry ;; ediff settings (setq ediff-diff-options "-w") ;; fewer backslashes in regexp builder (require 're-builder) (setq reb-re-syntax 'string) ;; remove prompt on killing process buffer (setq kill-buffer-query-functions (remq 'process-kill-buffer-query-function kill-buffer-query-functions)) ;; enable features that are disabled by default (put 'narrow-to-region 'disabled nil) (put 'erase-buffer 'disabled nil) (put 'narrow-to-page 'disabled nil) (add-hook 'prog-mode-hook 'display-fill-column-indicator-mode) ;; expand region (global-set-key (kbd "M-@") 'er/expand-region) ;; Swap C-t and C-x, so it's easier to type on Dvorak layout ;; `keyboard-translate` does not work when attaching an emacsclient to ;; a running emacs in daemon mode, so instead we define the key in the ;; key-translation-map. ;; http://lists.gnu.org/archive/html/help-gnu-emacs/2009-10/msg00505.html (define-key key-translation-map [?\C-x] [?\C-t]) (define-key key-translation-map [?\C-t] [?\C-x]) ;; Use narrow tab width (set-default 'tab-width 4) (setq tab-width 4) ;; disable away timestamp in ERC (setq erc-away-timestamp-format nil) (setq erc-timestamp-format nil) ;; don’t switch to a newly created IRC buffer (setq erc-join-buffer 'bury) ;; Revert stale document graphics buffers automatically when the files ;; have changed. (add-hook 'doc-view-mode-hook 'auto-revert-mode) (page-break-lines-mode 1) (global-set-key (kbd "") 'backward-page) (global-set-key (kbd "") 'forward-page) (add-to-list 'auto-mode-alist '("\\.html\\'" . sgml-mode)) (eval-when-compile (require 'tagedit)) (eval-after-load "sgml-mode" '(progn (require 'tagedit) (tagedit-add-paredit-like-keybindings) (tagedit-add-experimental-features) (add-hook 'html-mode-hook (lambda () (tagedit-mode 1))))) (delete-selection-mode 1) ; delete seleted text when typing ;; don't let the cursor go into minibuffer prompt, HT Xah Lee (setq minibuffer-prompt-properties '(read-only t point-entered minibuffer-avoid-prompt face minibuffer-prompt)) (savehist-mode) ;; PDF view mode (setq pdf-info-epdfinfo-program "~/.guix-home/emacs-profile/bin/epdfinfo") (pdf-tools-install) (add-to-list 'auto-mode-alist '("\\.pdf\\'" . pdf-view-mode)) ;; enable variable-pitch-mode in eww (add-hook 'eww-mode-hook (lambda () (variable-pitch-mode 1) (olivetti-mode 1))) #+END_SRC Here are a few commands that I used pretty often: #+BEGIN_SRC elisp (defun my/new-empty-buffer () "Open a new empty buffer." (interactive) (let ((buf (generate-new-buffer "untitled"))) (switch-to-buffer buf) (funcall (and initial-major-mode)) (setq buffer-offer-save t))) (global-set-key (kbd "C-c n") 'my/new-empty-buffer) ;; http://whattheemacsd.com/key-bindings.el-01.html#disqus_thread (defun my/goto-line-with-feedback () "Show line numbers temporarily, while prompting for the line number input" (interactive) (let ((line-numbers-off-p (not display-line-numbers-mode))) (unwind-protect (progn (when line-numbers-off-p (display-line-numbers-mode 1)) (call-interactively 'goto-line)) (when line-numbers-off-p (display-line-numbers-mode -1))))) (global-set-key [remap goto-line] 'my/goto-line-with-feedback) ;; kill current buffer (global-set-key (kbd "C-x C-k") (lambda () (interactive) (kill-buffer (current-buffer)))) ;; delete up to non-whitespace character (global-set-key (kbd "C-c d") (lambda () (interactive) (cycle-spacing -1))) (defun ssh-dtach (host) "Open SSH connection to HOST and start dtach session." (interactive "sHost: ") (let ((explicit-shell-file-name "dtach") (explicit-dtach-args '("-A" "/tmp/emacs.dtach" "-z" "/bin/bash" "--noediting" "-login")) (default-directory (format "/ssh:%s:" host))) (shell (format "*ssh %s*" host)))) ;; http://blog.vivekhaldar.com/post/4809065853/dotemacs-extract-interactively-change-font-size (defun my/zoom-in () "Increase font size by 10 points" (interactive) (set-face-attribute 'default nil :height (+ (face-attribute 'default :height) 10))) (defun my/zoom-out () "Decrease font size by 10 points" (interactive) (set-face-attribute 'default nil :height (- (face-attribute 'default :height) 10))) ;; change font size, interactively (global-set-key (kbd "C->") 'my/zoom-in) (global-set-key (kbd "C-<") 'my/zoom-out) ;; easier way to jump to other window (global-set-key (kbd "M-o") 'other-window) (defun my/smart-open-line () "Insert an empty line after the current line. Position the cursor at its beginning, according to the current mode." (interactive) (move-end-of-line nil) (newline-and-indent)) (global-set-key [(shift return)] 'my/smart-open-line) ;; http://stackoverflow.com/a/18814469/519736 (defun my/copy-buffer-file-name (choice) "Copy the buffer-file-name to the kill-ring" (interactive "cCopy Buffer Name (F) Full, (D) Directory, (N) Name") (let ((new-kill-string) (name (if (eq major-mode 'dired-mode) (dired-get-filename) (or (buffer-file-name) "")))) (cond ((eq choice ?f) (setq new-kill-string name)) ((eq choice ?d) (setq new-kill-string (file-name-directory name))) ((eq choice ?n) (setq new-kill-string (file-name-nondirectory name))) (t (message "Quit"))) (when new-kill-string (message "%s copied" new-kill-string) (kill-new new-kill-string)))) #+END_SRC * Putting it all together Having defined named code blocks in the sections above we can finally put them all together to build the init file #+BEGIN_SRC elisp (global-set-key (kbd "C-x RET 1") (lambda () (interactive) (insert "¯\\_(ツ)_/¯"))) #+END_SRC