Skip to content
forked from munen/emacs.d

My emacs configuration documented in literate programming style

License

Notifications You must be signed in to change notification settings

phiadaarr/emacs.d

 
 

Repository files navigation

Emacs configuration

This repository contains my Emacs configuration. It is written and documented in literate programming style.

Play Emacs like an instrument

If you’re new to Emacs and just want to have a look around: Lean back and relax while enjoying a deep dive into the wonderful world of the Emacs editor. I have a talk “Play Emacs like an instrument” which is a small teaser of what Emacs can do - and what kinds of features you’ll find in this repository: https://www.youtube.com/watch?v=gfZDwYeBlO4

Installation

Initial

Emacs configuration is usually done in the home directory in the .emacs.d folder. This holds true for Unix and Linux systems. For Windows, look it up here.

git clone [email protected]:munen/emacs.d.git ~/.emacs.d

Dependencies

Emacs dependencies/libraries are managed via the internal package management system. To initially install packages, open ~/.emacs.d/init.el, refresh your package list with M-x package-refresh-contents and install everything using M-x eval-buffer.

Dependency management

Define package repositories(archives)

(require 'package)

(setq package-archives '(("gnu" . "https://elpa.gnu.org/packages/")
                         ("org" . "https://orgmode.org/elpa/")
                         ("melpa" . "https://melpa.org/packages/")))

Define packages that are to be installed

List all used third-party packages. Most will be configured further down in this file, some are used with the default configuration.

(defvar my-packages '(ag
                      ;;atomic-chrome
                      auto-complete
                      beacon
                      bicycle
                      browse-kill-ring
                      comment-tags
                      darktooth-theme
                      dired-narrow
                      diminish
                      dumb-jump
                      edit-indirect
                      editorconfig
                      elfeed
                      elfeed-goodies
                      erc-image
                      evil
                      evil-escape
                      evil-leader
                      evil-mc
                      evil-numbers
                      evil-surround
                      exec-path-from-shell
                      forge
                      flycheck
                      hide-mode-line
                      impatient-mode
                      ivy counsel swiper
                      ;; commandline accounting ledger-mode
                      magit
                      markdown-mode
                      package-lint
                      parinfer
                      pdf-tools
                      projectile
                      rainbow-mode
                      sass-mode
                      spacemacs-theme
                      spaceline
                      smex
                      synosaurus
                      tide
                      visual-fill-column
                      which-key
                      writegood-mode
                      writeroom-mode
                      yaml-mode
                      zenburn-theme))

Install packages

(dolist (p my-packages)
  (unless (package-installed-p p)
    (package-refresh-contents)
    (package-install p))
  (add-to-list 'package-selected-packages p))

Default Settings

This section contains settings for built-in Emacs features.

Gnu Elpa TLS Fix

Emacs 26.1 (for example in Debian Buster) requests the GNU Elpa repo with the wrong TLS version - which makes the request fail. This is a manual patch for older versions of Emacs. It’s fixed from 26.3 and above upstream.

(if (string< emacs-version
         "26.3")
      (setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3"))

Garbage Collection

Allow 20MB of memory (instead of 0.76MB) before calling garbage collection. This means GC runs less often, which speeds up some operations.

(setq gc-cons-threshold 20000000)

Do not create backup files

(setq make-backup-files nil)

Warn when opening big files

The default warning comes with a 10MB file size which my machine handles with no noticeable delay at all. Hence, only warn when opening files bigger than 200MB.

(setq large-file-warning-threshold 200000000)

Auto-Save in /tmp

Store backups and auto-saved files in TEMPORARY-FILE-DIRECTORY (which defaults to /tmp on Unix), instead of in the same directory as the file.

(setq backup-directory-alist
      `((".*" . ,temporary-file-directory)))
(setq auto-save-file-name-transforms
      `((".*" ,temporary-file-directory t)))

Always follow symlinks

When opening a file, always follow symlinks.

(setq vc-follow-symlinks t)

Sentences have one space after a period

Don’t assume that sentences should have two spaces after periods.

(setq sentence-end-double-space nil)

Confirm before closing Emacs

(setq confirm-kill-emacs 'y-or-n-p)

dired-mode

Ability to use a to visit a new directory or file in dired instead of using RET. RET works just fine, but it will create a new buffer for every interaction whereas a reuses the current buffer.

(put 'dired-find-alternate-file 'disabled nil)

Human readable units

(setq-default dired-listing-switches "-alh")

On C, recursively copy by default

(setq dired-recursive-copies 'always)

dired-narrow

dired-narrow of the dired-hacks repository allows to dynamically narrow a dired buffer down to contents of interest. A demo can be seen on this blog post.

(require 'dired)
(define-key dired-mode-map (kbd "/") 'dired-narrow-fuzzy)

Commands:

  • / starts fuzzy matching
  • Use the dired buffer as usual
  • g to go back to the complete file listing

Ask y/n instead of yes/no

This is a favorable shorthand.

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

Auto revert files on change

When something changes a file, automatically refresh the buffer containing that file so they can’t get out of sync.

(global-auto-revert-mode t)

Shortcut for changing font-size

(defun zoom-in ()
  (interactive)
  (let ((x (+ (face-attribute 'default :height)
              10)))
    (set-face-attribute 'default nil :height x)))

(defun zoom-out ()
  (interactive)
  (let ((x (- (face-attribute 'default :height)
              10)))
    (set-face-attribute 'default nil :height x)))

(define-key global-map (kbd "C-1") 'zoom-in)
(define-key global-map (kbd "C-0") 'zoom-out)

Disable startup message

(setq inhibit-splash-screen t)
(setq inhibit-startup-message t)

Display the current time

(display-time-mode t)

Do not display GUI Toolbar

(tool-bar-mode 0)

Automatic Line Breaks

Do not enable automatic line breaks for all text-mode based hooks, because several text-modes (markdown, mails) enjoy the pain of long lines. So here, I only add whitelisted modes sparingly. The other modes have a visual-line-mode= configuration which makes the text look nice locally, at least.

(add-hook 'org-mode-hook 'auto-fill-mode)

visual-fill-column-mode

https://github.com/joostkremers/visual-fill-column

visual-fill-column-mode is a small Emacs minor mode that mimics the effect of fill-column in visual-line-mode. Instead of wrapping lines at the window edge, which is the standard behaviour of visual-line-mode, it wraps lines at fill-column. If fill-column is too large for the window, the text is wrapped at the window edge.

Enable whenever upstream visual-line-mode is activated.

(add-hook 'visual-line-mode-hook #'visual-fill-column-mode)
(add-hook 'visual-line-mode-hook #'adaptive-wrap-prefix-mode)

Enable visual-fill-mode for all text based modes:

;; Don't do it at this time, it's only enabled for some modes explicitly.
;; (add-hook 'text-mode-hook 'visual-line-mode)

Enable adative-wrap-prefix-mode:

https://elpa.gnu.org/packages/adaptive-wrap.html

This package provides the `adaptive-wrap-prefix-mode’ minor mode which sets the wrap-prefix property on the fly so that single-long-line paragraphs get word-wrapped in a way similar to what you’d get with M-q using adaptive-fill-mode, but without actually changing the buffer’s text.

(add-hook 'visual-line-mode-hook #'adaptive-wrap-prefix-mode)

Enable Narrow To Region

Enable narrow-to-region (C-x n n / C-x n w). This is disabled by default to not confuse beginners.

(put 'narrow-to-region 'disabled nil)

Disable scroll bars

(scroll-bar-mode -1)

Remember the cursor position of files when reopening them

(setq save-place-file "~/.emacs.d/saveplace")
(if (version<= emacs-version "25.1")
    (progn
      (setq-default save-place t)
      (require 'saveplace))
  (save-place-mode 1))

Set $MANPATH, $PATH and exec-path from shell even when started from GUI helpers like dmenu or Spotlight

;; Safeguard, so this only runs on Linux (or MacOS)
(when (memq window-system '(mac ns x))
  (exec-path-from-shell-initialize))

windmove

Windmove is built into Emacs. It lets you move point from window to window using Shift and the arrow keys. This is easier to type than ‘C-x o’ when there are multiple windows open.

(when (fboundp 'windmove-default-keybindings)
  (windmove-default-keybindings))

winner-mode

Allows to ‘undo’ (and ‘redo’) changes in the window configuration with the key commands ‘C-c left’ and ‘C-c right’.

(when (fboundp 'winner-mode)
  (winner-mode 1))

Getting from many windows to one window is easy: ‘C-x 1’ will do it. But getting back to a delicate WindowConfiguration is difficult. This is where Winner Mode comes in: With it, going back to a previous session is easy.

Bell

Do not ring the system bell, but show a visible feedback.

(setq visible-bell t)

AngeFtp

Try to use passive mode for FTP.

Note: Some firewalls might not allow standard active mode. However: Some FTP Servers might not allow passive mode. So if there’s problems when connecting to an FTP, try to revert to active mode.

(setq ange-ftp-try-passive-mode t)

eww

When entering eww, use cursors to scroll without changing point.

(add-hook 'eww-mode-hook 'scroll-lock-mode)

Custom-File

(setq custom-file "~/.emacs.d/custom-settings.el")
(load custom-file t)

Add guix packages to load-path

I’m running Debian and for some things I use GNU Guix for package management. For example mu4e is installed through guix, so that I can always have a recent version. This adds the installed packages to the standard Emacs load path, so that require just works.

(add-to-list 'load-path "/home/munen/.guix-profile/share/emacs/site-lisp/")
(add-to-list 'load-path "/usr/share/emacs/site-lisp/mu4e")

Misc Custom Improvements

Some helper functions and packages I wrote that are only accessible within this Git repository and not published to a package repository.

Translations

Elisp wrapper around the dict.cc translation service. Translations are exposed in an org-mode table.

Demo: https://asciinema.org/a/hMTM9EDHE0cphaDRFr4JXr1iw.png

Load dict.el

(load "~/.emacs.d/dict")

Helper functions to clean up the gazillion buffers

When switching projects in Emacs, it can be prudent to clean up every once in a while. Deleting all buffers except the current one is one of the things I often do (especially in the long-running emacsclient).

(defun kill-other-buffers ()
  "Kill all other buffers."
  (interactive)
  (mapc 'kill-buffer (delq (current-buffer) (buffer-list))))

dired will create buffers for every visited folder. This is a helper to clear them out once you’re done working with those folders.

(defun kill-dired-buffers ()
  "Kill all open dired buffers."
  (interactive)
  (mapc (lambda (buffer)
          (when (eq 'dired-mode (buffer-local-value 'major-mode buffer))
            (kill-buffer buffer)))
        (buffer-list)))

Encode HTML to HTML entities

Rudimentary function converting certain HTML syntax to HTML entities.

(defun encode-html (start end)
  "Encodes HTML entities; works great in Visual Mode (START END)."
  (interactive "r")
  (save-excursion
    (save-restriction
      (narrow-to-region start end)
      (goto-char (point-min))
      (replace-string "&" "&amp;")
      (goto-char (point-min))
      (replace-string "<" "&lt;")
      (goto-char (point-min))
      (replace-string ">" "&gt;"))))

Convenience functions when working with PDF exports

When working on markdown or org-mode files that will be converted to PDF, I use pdf-tools to preview the PDF and shortcuts to automatically save, compile and reload on demand.

Here is a screencast showing how I edit Markdown or org-mode files in Emacs whilst having a PDF preview.

In a screenshot, it looks like this:

images/edit_markup_with_preview.png

(defun md-compile ()
  "Compiles the currently loaded markdown file using pandoc into a PDF"
  (interactive)
  (save-buffer)
  (shell-command (concat "pandoc " (buffer-file-name) " -o "
                         (replace-regexp-in-string "md" "pdf" (buffer-file-name)))))

(defun update-other-buffer ()
  (interactive)
  (other-window 1)
  (revert-buffer nil t)
  (other-window -1))

(defun md-compile-and-update-other-buffer ()
  "Has as a premise that it's run from a markdown-mode buffer and the
   other buffer already has the PDF open"
  (interactive)
  (md-compile)
  (update-other-buffer))

(defun latex-compile-and-update-other-buffer ()
  "Has as a premise that it's run from a latex-mode buffer and the
   other buffer already has the PDF open"
  (interactive)
  (save-buffer)
  (shell-command (concat "pdflatex " (buffer-file-name)))
  (switch-to-buffer (other-buffer))
  (kill-buffer)
  (update-other-buffer))

(defun org-compile-beamer-and-update-other-buffer ()
  "Has as a premise that it's run from an org-mode buffer and the
   other buffer already has the PDF open"
  (interactive)
  (org-beamer-export-to-pdf)
  (update-other-buffer))

(defun org-compile-latex-and-update-other-buffer ()
  "Has as a premise that it's run from an org-mode buffer and the
   other buffer already has the PDF open"
  (interactive)
  (org-latex-export-to-pdf)
  (update-other-buffer))

(eval-after-load 'latex-mode
  '(define-key latex-mode-map (kbd "C-c r") 'latex-compile-and-update-other-buffer))

(define-key org-mode-map (kbd "C-c lr") 'org-compile-latex-and-update-other-buffer)
(define-key org-mode-map (kbd "C-c br") 'org-compile-beamer-and-update-other-buffer)

(eval-after-load 'markdown-mode
  '(define-key markdown-mode-map (kbd "C-c r") 'md-compile-and-update-other-buffer))

Use left Cmd to create Umlauts

Unrelated to Emacs, in macOS, you can write Umlauts by using the combo M-u [KEY]. For example M-u u will create the letter ü.

This is actually faster than the default way of Emacs or that of VIM. The following code ports that functionality to Emacs.

Thx @jcfischer for the function!

(define-key key-translation-map [dead-diaeresis]
  (lookup-key key-translation-map "\C-x8\""))
(define-key isearch-mode-map [dead-diaeresis] nil)
(global-set-key (kbd "M-u")
                (lookup-key key-translation-map "\C-x8\""))

Generate passwords

Through pwgen.

Thanks to @branch14 of 200ok fame for the function!

(defun generate-password-non-interactive ()
   (string-trim (shell-command-to-string "pwgen -A 24")))

(defun generate-password ()
  "Generates and inserts a new password"
  (interactive)
  (insert
   (shell-command-to-string
    (concat "pwgen -A " (read-string "Length: " "24") " 1"))))

Passwords file

Open the GPG encrypted password file.

Within this file, I’ll search for passwords with counsel-imenu which has nice auto-completion and means that the headers will always be folded, so that no other person can see the passwords.

When the right header is found, I’ll copy the password under the current header to the clipboard from where I can use it where I need it (for example a browser):

Copy password to clipboard

(fset 'copy-password-to-clipboard
   [?\C-s ?P ?a ?s ?s ?w ?o ?r ?d ?: return ?w ?v ?$ ?y C-up C-up C-up tab])

Open passwords file

(defun passwords ()
  "Open main 'passwords' file."
  (interactive)
  (find-file (concat org-directory "vault/primary.org.gpg")))

Running M-x shell with zsh

If you’re a zsh user, you might have configured a custom prompt and such. Also, you might be using a powerful $TERM for that. When running zsh within M-x shell, you will have to set the $TERM to dumb, though. Otherwise you’ll get all kinds of escape sequences instead of colored text.

I’m using this within my ~/.zshrc

# This allows running `shell` properly within Emacs
if [ -n "$INSIDE_EMACS" ]; then
  export TERM=dumb
else
  export TERM=xterm-256color
fi

server-shutdown

This is the converse function to the built-in server-start.

(defun server-shutdown ()
  "Save buffers, Quit, and Shutdown (kill) server"
  (interactive)
  (save-some-buffers)
  (kill-emacs))

Helper function to measure the running time of a function

(defmacro measure-time (&rest body)
  "Measure the time it takes to evaluate BODY."
  `(let ((time (current-time)))
     ,@body
     (message "%.06f" (float-time (time-since time)))))

For example (measure-time (prettier-eslint).

Sudo Save

If the current buffer is not writable, ask if it should be saved with sudo.

Happily taken from Pascals configuration: https://github.com/SirPscl/emacs.d#sudo-save

(defun ph/sudo-file-name (filename)
  "Prepend '/sudo:root@`system-name`:' to FILENAME if appropriate.
This is, when it doesn't already have a sudo-prefix."
  (if (not (or (string-prefix-p "/sudo:root@localhost:"
                                filename)
               (string-prefix-p (format "/sudo:root@%s:" system-name)
                                filename)))
      (format "/sudo:root@%s:%s" system-name filename)
    filename))

(defun ph/sudo-save-buffer ()
  "Save FILENAME with sudo if the user approves."
  (interactive)
  (when buffer-file-name
    (let ((file (ph/sudo-file-name buffer-file-name)))
      (if (yes-or-no-p (format "Save file as %s ? " file))
          (write-file file)))))

(advice-add 'save-buffer :around
            '(lambda (fn &rest args)
               (when (or (not (buffer-file-name))
                         (not (buffer-modified-p))
                         (file-writable-p (buffer-file-name))
                         (not (ph/sudo-save-buffer)))
                 (call-interactively fn args))))

Open file with emacsclient using filename:line path

This configuration is originally from the great bbatsov’s prelude.

“`bash emacsclient somefile:1234 “`

This will open file ‘somefile’ and set cursor on line 1234.

(defadvice server-visit-files (before parse-numbers-in-lines (files proc &optional nowait) activate)
  "Open file with emacsclient with cursors positioned on requested line.
Most of console-based utilities prints filename in format
'filename:linenumber'.  So you may wish to open filename in that format.
Just call:
  emacsclient filename:linenumber
and file 'filename' will be opened and cursor set on line 'linenumber'"
  (ad-set-arg 0
              (mapcar (lambda (fn)
                        (let ((name (car fn)))
                          (if (string-match "^\\(.*?\\):\\([0-9]+\\)\\(?::\\([0-9]+\\)\\)?$" name)
                              (cons
                               (match-string 1 name)
                               (cons (string-to-number (match-string 2 name))
                                     (string-to-number (or (match-string 3 name) ""))))
                            fn))) files)))

Emacs takes SVG screenshot of itself

;; https://www.reddit.com/r/emacs/comments/idz35e/emacs_27_can_take_svg_screenshots_of_itself/
(defun screenshot-svg ()
  "Save a screenshot of the current frame as an SVG image.
Saves to a temp file and puts the filename in the kill ring."
  (interactive)
  (let* ((filename (make-temp-file "Emacs" nil ".svg"))
         (data (x-export-frames nil 'svg)))
    (with-temp-file filename
      (insert data))
    (kill-new filename)
    (message filename)))

Search non-ASCII characters

isearch can find a wide range of Unicode characters (like á, ⓐ, or 𝒶) when you search for ASCII characters (a in this example).

(setq search-default-mode #'char-fold-to-regexp)

General

This section contains settings for non-built-in Emacs features that are generally applicable to different kinds of modes.

beacon-mode

https://github.com/Malabarba/beacon

Whenever the window scrolls a light will shine on top of your cursor so you know where it is.

(beacon-mode 1)

browse-kill-ring

Ever wish you could just look through everything you’ve killed recently to find out if you killed that piece of text that you think you killed (or yanked), but you’re not quite sure? If so, then browse-kill-ring is the Emacs extension for you.

(require 'browse-kill-ring)
(setq browse-kill-ring-highlight-inserted-item t
      browse-kill-ring-highlight-current-entry nil
      browse-kill-ring-show-preview t)
(define-key browse-kill-ring-mode-map (kbd "j") 'browse-kill-ring-forward)
(define-key browse-kill-ring-mode-map (kbd "k") 'browse-kill-ring-previous)

evil-mode

Evil is an extensible Vim layer for Emacs.

This combines the best of both worlds: VIM being a great text-editor with modal editing through semantic commands and Emacs being a LISP REPL.

Enable Evil

(evil-mode t)
;; Enable "M-x" in evil mode
(global-set-key (kbd "M-x") 'execute-extended-command)

Leader Mode Config

(global-evil-leader-mode)
(evil-leader/set-leader ",")
(evil-leader/set-key
  "w" 'basic-save-buffer
  "s" 'flyspell-buffer
  "b" 'evil-buffer
  "q" 'evil-quit)

Evil Surround, emulating tpope’s surround.vim

(require 'evil-surround)
(global-evil-surround-mode 1)

Multiple Cursors

https://github.com/gabesoft/evil-mc

evil-mc provides multiple cursors functionality for Emacs when used with evil-mode.

C-n / C-p are used for creating cursors, and M-n / M-p are used for cycling through cursors. The commands that create cursors wrap around; but, the ones that cycle them do not. To skip creating a cursor forward use C-t or grn and backward grp. Finally use gru to remove all cursors.

Enable evil-mc for all buffers

(global-evil-mc-mode  1)

Fast switching between buffers

(define-key evil-normal-state-map (kbd "{") 'evil-next-buffer)
(define-key evil-normal-state-map (kbd "}") 'evil-prev-buffer)

Increment / Decrement numbers

(global-set-key (kbd "C-=") 'evil-numbers/inc-at-pt)
(global-set-key (kbd "C--") 'evil-numbers/dec-at-pt)
(define-key evil-normal-state-map (kbd "C-=") 'evil-numbers/inc-at-pt)
(define-key evil-normal-state-map (kbd "C--") 'evil-numbers/dec-at-pt)

Use j/k for browsing wrapped lines

(define-key evil-normal-state-map (kbd "j") 'evil-next-visual-line)
(define-key evil-normal-state-map (kbd "k") 'evil-previous-visual-line)

Paste in Visual Mode

(define-key evil-insert-state-map (kbd "C-v") 'evil-visual-paste)

Disable evil-mode for some modes

Since Emacs is a multi-purpose LISP REPL, there are many modes that are not primarily (or not at all) centered about text-manipulation. For those, it is reasonable to disable evil-mode, because it will bring nothing to the table, but might just shadow some keyboard shortcuts.

(mapc (lambda (mode)
        (evil-set-initial-state mode 'emacs)) '(elfeed-show-mode
                                                elfeed-search-mode
                                                forge-pullreq-list-mode
                                                forge-topic-list-mode
                                                dired-mode
                                                tide-references-mode
                                                image-dired-mode
                                                image-dired-thumbnail-mode
                                                eww-mode))

Turning off evil when working in cider--debug minor mode:

(defadvice cider--debug-mode (after toggle-evil activate)
  "Turn off `evil-local-mode' when enabling
`cider--debug-mode', and turn it back on when disabling
`cider--debug-mode'."
  (evil-local-mode (if cider--debug-mode -1 1)))

Unbind M-. and M- in evil-mode

M-. and M-, are popular keybindings for “jump to definition” and “back”. evil-mode by default binds those to rather rarely used functions evil-repeat-pop-next and xref-pop-marker-stack, for some reason.

(define-key evil-normal-state-map (kbd "M-.") nil)
(define-key evil-normal-state-map (kbd "M-,") nil)

Call ex by default on visual selection

(setq evil-ex-visual-char-range t)

Example:

When visually selecting “foo” out of the string “foo foobar”, and then calling :s/o/i/g, the result would be “fii fiibar” without this setting. With this setting, it will be “fii foobar”.

evil-escape

https://github.com/syl20bnr/evil-escape

Escape from insert state and everything else.

(setq-default evil-escape-delay 0.2)
(setq-default evil-escape-key-sequence "jk")
(evil-escape-mode)

This results in the same feature-set like this vim keybinding:

"Remap ESC to jk
:imap jk <esc>

Which Key

which-key displays available keybindings in a popup.

(add-hook 'org-mode-hook 'which-key-mode)
(add-hook 'cider-mode-hook 'which-key-mode)

Use which-key to show VIM shortcuts, too.

(setq which-key-allow-evil-operators t)
(setq which-key-show-operator-state-maps t)

Programming

General

Auto Complete

https://github.com/auto-complete/auto-complete

Basic Configuration

(ac-config-default)

Tabs

Set tab width to 2 for all buffers

(setq-default tab-width 2)

Use 2 spaces instead of a tab.

(setq-default tab-width 2 indent-tabs-mode nil)

Indentation cannot insert tabs.

(setq-default indent-tabs-mode nil)

Use 2 spaces instead of tabs for programming languages.

(setq js-indent-level 2)

(setq coffee-tab-width 2)

(setq python-indent 2)

(setq css-indent-offset 2)

(add-hook 'sh-mode-hook
          (lambda ()
            (setq sh-basic-offset 2
                  sh-indentation 2)))

(setq web-mode-markup-indent-offset 2)

Syntax Checking (flycheck)

http://www.flycheck.org/

Enable global on the fly syntax checking through flycheck.

(add-hook 'after-init-hook #'global-flycheck-mode)

Manage TODO/FIXME/XXX comments

https://github.com/vincekd/comment-tags

comment-tags highlights and lists comment tags such as ‘TODO’, ‘FIXME’, ‘XXX’.

Commands (prefixed by C-c t):

  • b to list tags in current buffer (comment-tags-list-tags-buffer).
  • a to list tags in all buffers (comment-tags-list-tags-buffers).
  • s to jump to tag in current buffer by a word or phrase using reading-completion (comment-tags-find-tags-buffer).
  • n to jump to next tag from point (comment-tags-next-tag).
  • p to jump to previous tag from point (comment-tags-previous-tag).
(setq comment-tags-keymap-prefix (kbd "C-c t"))
(with-eval-after-load "comment-tags"
  (setq comment-tags-keyword-faces
        `(;; A concrete TODO with actionable steps
          ("TODO" . ,(list :weight 'bold :foreground "#DF5427"))
          ;; A non-concrete TODO. We only know something is broken/amiss.
          ("FIXME" . ,(list :weight 'bold :foreground "#DF5427"))
          ;; Works, but is a code smell (quick fix). Might break down the line.
          ("HACK" . ,(list :weight 'bold :foreground "#DF5427"))
          ;; Assumption that needs to be verified.
          ("CHECK" . ,(list :weight 'bold :foreground "#CC6437"))
          ;; Use to highlight a regular, but especially important, comment.
          ("NOTE" . ,(list :weight 'bold :foreground "#1FDA9A"))
          ;; Use to highlight a regular, but especially important, comment.
          ("INFO" . ,(list :weight 'bold :foreground "#1FDA9A"))))
  (setq comment-tags-comment-start-only t
        comment-tags-require-colon t
        comment-tags-case-sensitive t
        comment-tags-show-faces t
        comment-tags-lighter nil))
(add-hook 'prog-mode-hook 'comment-tags-mode)
(add-hook 'conf-mode-hook 'comment-tags-mode)

Auto-indent with the Return key

(define-key global-map (kbd "RET") 'newline-and-indent)

Highlight matching parenthesis

(show-paren-mode t)

Delete trailing whitespace

Delete trailing whitespace in all modes. Except when editing Markdown, because it uses two trailing blanks as a signal to create a line break.

(add-hook 'before-save-hook '(lambda()
                              (when (not (or (derived-mode-p 'markdown-mode)))
                                (delete-trailing-whitespace))))

Code Folding

Enable code folding for programming modes with two strategies:

1. VIM style folds

  • zc: Close fold (one)
  • za: Toggle fold (one)
  • zr: Open folds (all)
  • zm: Close folds (all)
(add-hook 'prog-mode-hook #'hs-minor-mode)

2. Org mode style folds with outline-minor-mode

outline-minor-mode is built-in to Emacs. It enables structural editing of hierarchical structures - just as Org mode does, but in any major mode.

Change the shortcuts to be the same as in Org mode:

(add-hook 'prog-mode-hook #'outline-minor-mode)

;; Org mode style keybindings
(define-key outline-minor-mode-map (kbd "C-<return>") 'outline-insert-heading)
(define-key outline-minor-mode-map (kbd "M-S-<right>") 'outline-demote)
(define-key outline-minor-mode-map (kbd "M-S-<left>") 'outline-promote)
(define-key outline-minor-mode-map (kbd "C-c C-n") 'outline-next-visible-heading)
(define-key outline-minor-mode-map (kbd "C-c C-p") 'outline-previous-visible-heading)

Leverage the bicycle library from tarsius for the ability to cycle visibility of local and global sections:

(define-key outline-minor-mode-map (kbd "C-<tab>") 'bicycle-cycle)
(define-key outline-minor-mode-map (kbd "<backtab>") 'bicycle-cycle-global)

Use the built-in foldout.el to narrow and widen the current subtree:

(require 'foldout)
(define-key outline-minor-mode-map (kbd "C-x n s") 'foldout-zoom-subtree)
(define-key outline-minor-mode-map (kbd "C-x n w") 'foldout-exit-fold)

Line numbers

Enable linum-mode for programming modes. For newer versions of Emacs, use display-line-numbers-mode, because it’s much faster.

(add-hook 'prog-mode-hook '(lambda ()
                             (if (version<= emacs-version "26.0.50")
                                 (linum-mode)
                               (display-line-numbers-mode))))

Indenting a buffer

(defun indent-buffer ()
  (interactive)
  (save-excursion
    (indent-region (point-min) (point-max) nil)))

p_slides

p_slides is a static files only, dead simple way, to create semantic slides. The slide content is markdown, embedded in a HTML file. When opening a presentation.html file, enable markdown-mode.

(add-to-list 'auto-mode-alist '("presentation.html" . markdown-mode))

yaml

(require 'yaml-mode)
(add-to-list 'auto-mode-alist '("\\.yml$" . yaml-mode))

Markdown

(add-hook 'markdown-mode-hook 'flyspell-mode)
(add-hook 'markdown-mode-hook 'outline-minor-mode)

Unfortunately line breaks are semantic in some versions of markdown (for example Github). So doing automatic line breaks would be harmful. However, this leads to super long lines in many documents which is unreadable. Therefore, always use visual-line-mode.

(add-hook 'markdown-mode-hook 'visual-line-mode)

Magit

https://github.com/magit/magit

Magit is an interface to the version control system Git.

Configuration

Create shortcut for Magit.

(global-set-key (kbd "C-x g") 'magit-status)

Always sign commits with GPG

(setq magit-commit-arguments (quote ("--gpg-sign=137099B38E1FC0E9")))

Start the commit buffer in evil normal mode

(add-hook 'with-editor-mode-hook 'evil-normal-state)

Forge

https://github.com/magit/forge/

Work with Git forges from the comfort of Magit.

(with-eval-after-load 'magit
  (require 'forge))

Add 200ok gitlab instance to list of known forges

(with-eval-after-load 'forge
  (add-to-list 'forge-alist
               '("gitlab.200ok.ch"
                 "gitlab.200ok.ch/api/v4"
                 "gitlab.200ok.ch"
                 forge-gitlab-repository))
  (add-to-list 'forge-alist
               '("gitlab.switch.ch"
                 "gitlab.switch.ch/api/v4"
                 "gitlab.switch.ch"
                 forge-gitlab-repository)))

Show assigned issues and PRs directly in the status buffer:

(with-eval-after-load 'magit
  (magit-add-section-hook 'magit-status-sections-hook 'forge-insert-assigned-issues   nil t)
  (magit-add-section-hook 'magit-status-sections-hook 'forge-insert-assigned-pullreqs   nil t))

Projectile

https://github.com/bbatsov/projectile

Projectile is a project interaction library. For instance - finding project files (C-c p f) or jumping to a new project (C-c p p).

Configuration

Enable Projectile globally

(projectile-mode +1)
(define-key projectile-mode-map (kbd "C-c p") 'projectile-command-map)

Dumb Jumb

https://github.com/jacktasia/dumb-jump

“Jump to definition” with support for multiple programming languages that favors “just working”. This means minimal – and ideally zero – configuration with absolutely no stored indexes (TAGS) or persistent background processes.

Dumb Jump uses The Silver Searcher ag, ripgrep rg, or grep to find potential definitions of a function or variable under point. It uses a set of regular expressions based on the file extension, or major-mode, of the current buffer.

(dumb-jump-mode)
(setq dumb-jump-selector 'ivy)

Usage

The one important shortcut is C-M-g which attempts to jump to the definition of the thing under point.

Code Styleguides

Auto-formatting

Automatically format code for different languages and frameworks.

This implements the interactive function autoformat which is a thin wrapper around command-line based code autoformatters which it utilizes through a strategy pattern.

To add a new language/framework, the only required change is to add the respective command-line tool configuration into a separate strategy function. It is trivial to do if the new language/framework has a command-line tool which takes code into stdin and formats it to stdout.

It’s possible to install the dependencies locally, so that the setup doesn’t impose dependencies on team members - or they can be installed through the respective packages managers (npm/yarn) to enforce code guidelines.

This requires prettier, @prettier/plugin-ruby and prettier-eslint-cli to be installed:

npm install -g prettier-eslint-cli prettier @prettier/plugin-ruby

Linting JavaScript with eslint happens automatically through flycheck. eslint just needs to be installed.

npm install -g eslint
(defun autoformat ()
  "Automatically format current buffer."
  (interactive)

  (if (derived-mode-p 'clojure-mode)
      (autoformat-clojure-function)
    (let ((eslint-path (concat (projectile-project-root)
                               ".eslintrc.yml"))) ; could be .json or .yml
      (autoformat-with
       (cond ((derived-mode-p 'web-mode) 'autoformat-html-command)
             ((derived-mode-p 'css-mode) 'autoformat-css-command)
             ((derived-mode-p 'json-mode) 'autoformat-json-command)
             ((derived-mode-p 'sass-mode) 'autoformat-sass-command)
             ((derived-mode-p 'yaml-mode) 'autoformat-yaml-command)
             ((derived-mode-p 'enh-ruby-mode) 'autoformat-ruby-command)
             ;; JS projects with eslint config
             ((and (file-exists-p eslint-path)
                   (derived-mode-p 'js2-mode))
              'autoformat-prettier-eslint-command)
             ((derived-mode-p 'js2-mode) 'autoformat-javascript-command))))))

(defun autoformat-with (strategy)
  "Automatically format current buffer using STRATEGY."
  (let ((p (point))
        (s (window-start)))
    ;; Remember the current position
    (save-mark-and-excursion
      ;; Call prettier-eslint binary with the contents of the current
      ;; buffer
      (shell-command-on-region
       (point-min) (point-max)
       (funcall strategy)
       ;; Write into a temporary buffer
       (get-buffer-create "*Temp autoformat buffer*")
       ;; Replace the current buffer with the output of
       ;; the =autoformat strategy= output
       t
       ;; If the =autoformat strategy= returns an error, show it in a
       ;; separate error buffer
       (get-buffer-create "*replace-errors*")
       ;; Automatically show error buffer
       t))
    ;; Return to the previous point and scrolling position (the point
    ;; was lost, because the whole buffer got replaced.
    (set-window-start (selected-window) s)
    (goto-char p)))

(defun autoformat-clojure-function ()
  "Cider function to format Clojure buffer."
  (indent-buffer)
  ;; (cider-format-buffer)
  )

(defun autoformat-ruby-command ()
  "CLI tool to format Ruby."
  "prettier --parser ruby")

(defun autoformat-javascript-command ()
  "CLI tool to format Javascript."
  "prettier --parser babel")

(defun autoformat-html-command ()
  "CLI tool to format HTML."
  "prettier --parser html")

(defun autoformat-css-command ()
  "CLI tool to format CSS."
  "prettier --parser css")

(defun autoformat-sass-command ()
  "CLI tool to format SASS."
  "prettier --parser sass")

(defun autoformat-json-command ()
  "CLI tool to format JSON."
  "prettier --parser json")

(defun autoformat-yaml-command ()
  "CLI tool to format YAML."
  "prettier --parser yaml")

(defun autoformat-prettier-eslint-command ()
  "CLI tool to format Javascript with .eslintrc.json configuration."
  (concat "npx prettier-eslint  --stdin --eslint-config-path="
          ;; Hand over the path of the current projec
          (concat
           (projectile-project-root)
           ".eslintrc.yml")
           " --stdin-filepath="
           (buffer-file-name)
          " --parser babel"))

Shortcut

(setq ok-autoformat-modes (list 'web-mode
                'css-mode
                'json-mode
                'clojure-mode
                'sass-mode
                'enh-ruby-mode
                'yaml-mode
                'js2-mode
                'rjsx-mode))

(dolist (mode ok-autoformat-modes)
  (evil-leader/set-key-for-mode mode "f" 'autoformat))

Demo

images/demo-ok-autoformat.gif

Call autoformat on every save - for certain projects

I don’t want to autoformat for every project, because I might not be the primary owner of the code (that accounts for consulting projects). However, there are projects where I actually do want to run autoformat every time. That is on projects with strict formatting requirements.

NB: The overhead of prettier + eslint is about 1.3s on a maxed out X1 Carbon 6th gen.

;; Define list of projects to autoformat
(setq ok-autoformat-projects (list "src/200ok/organice"))

(add-hook 'before-save-hook
          '(lambda()
             ;; Check if the current directory matches the list of
             ;; projects that are to be autoformatted.
             (if (seq-some '(lambda (e)
                              (numberp e))
                           (mapcar '(lambda (dir)
                                      (string-match dir (projectile-project-root)))
                                   ok-autoformat-projects) )
                 (when (or
                        (derived-mode-p 'js2-mode)
                        (derived-mode-p 'css-mode)
                        (derived-mode-p 'sass-mode)
                        (derived-mode-p 'yaml-mode))
                   (autoformat)))))
Alternative implementation

NB: This could be a good alternative solution. However, scoping to the local directory doesn’t work like this. Maybe I’m doing it wrong, maybe dir-locals just shouldn’t be used outside of setting variables.

Call autoformat on every save for specific projects

those projects, you can enable autoformat by creating a .dir-locals.el file in your home directory.

(("src"
  (nil .
       ((eval add-hook 'before-save-hook '(lambda()
                                              (autoformat)))))))

The first node “src/” is the directory, while the second node is the mode-name, or “nil” to apply to every mode.

Editorconfig

EditorConfig helps maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs. I’m an Emacs guy, however, when in an heterogeneous team, it does make sense to adhere to some commonly shared definitions.

With this plugin, if there is an .editorconfig in a project, the settings in this file will trump my personal config.

(editorconfig-mode 1)

Org mode

Outline-based notes management and organizer. It is an outline-mode for keeping track of everything.

Next to Emacs Org mode, I use organice (https://github.com/200ok-ch/organice/) to manage my Org files on the go and to collaborate with non-Emacs users.

General config

(setq org-directory "~/Dropbox/org/")

Plain Lists

Allow ‘a.’, ‘A.’, ‘a)’ and ‘A) as list elements:

(setq org-list-allow-alphabetical t)

Warn about an approaching deadline

The default is 14 days ahead. That’s way too much for me. If a task needs a lot of work ahead of the deadline, I’ll set a custom reminder date or an additional schedule.

(setq org-deadline-warning-days 3)

General configuration

(require 'org)

; languages for org-babel support
(org-babel-do-load-languages
 'org-babel-load-languages
 '(
   (shell . t)
   (dot . t)
   (js . t)
   (ruby . t)
   ))

(add-hook 'org-mode-hook 'auto-fill-mode)
(add-hook 'org-mode-hook 'flyspell-mode)

(evil-leader/set-key
  "a" 'org-archive-subtree-default)

;; Allow =pdflatex= to use shell-commands. This will allow it to use
;; =pygments= as syntax highlighter for exports to PDF.
;; (setq org-latex-pdf-process
;;       '("pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f"
;;         "pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f"
;;         "pdflatex -shell-escape -interaction nonstopmode -output-directory %o %f"))
;;  Alternatively use =xelatex=. Required for documents where I want to use ttf fonts.
(setq org-latex-pdf-process
      '("xelatex -shell-escape -interaction nonstopmode -output-directory %o %f"
        "xelatex -shell-escape -interaction nonstopmode -output-directory %o %f"
        "xelatex -shell-escape -interaction nonstopmode -output-directory %o %f"))

;; Include =minted= package for LaTeX exports
(add-to-list 'org-latex-packages-alist '("" "minted"))
(setq org-latex-listings 'minted)

;; Don’t ask every time when executing a code block.
(setq org-confirm-babel-evaluate nil)

imenu

imenu would normally only index two levels - since I run deeply nested documents, go up to six levels.

(setq org-imenu-depth 6)

When a document is folded and the user searches and finds with imenu, the body of the folded header is revealed, so that the search result can actually be seen.

(defun ok-imenu-show-entry ()
  "Reveal content of header."
  (cond
   ((and (eq major-mode 'org-mode)
         (org-at-heading-p))
    (org-show-entry)
    (org-reveal t))
   ((bound-and-true-p outline-minor-mode)
    (outline-show-entry))))

(add-hook 'imenu-after-jump-hook 'ok-imenu-show-entry)

KOMA Script export

(require 'ox-latex)
(add-to-list 'org-latex-classes
             '("scrartcl"
               "\\documentclass{scrartcl}"
               ("\\section{%s}" . "\\section*{%s}")))

Tufte org-mode export

(require 'ox-latex)
(add-to-list 'org-latex-classes
	     '("tuftehandout"
	       "\\documentclass{tufte-handout}
\\usepackage{color}
\\usepackage{amssymb}
\\usepackage{amsmath}
\\usepackage{gensymb}
\\usepackage{nicefrac}
\\usepackage{units}"
	       ("\\section{%s}" . "\\section*{%s}")
	       ("\\subsection{%s}" . "\\subsection*{%s}")
	       ("\\paragraph{%s}" . "\\paragraph*{%s}")
	       ("\\subparagraph{%s}" . "\\subparagraph*{%s}")))

Tags

Align tags to the far right of the screen. -77 would be good for a smaller 80 character terminal.

(setq org-tags-column -100)

Capture Templates

Set up capture templates for:
  • Todos which land in Inbox
  • Expenses which land in Inbox
  • Code Snippets which land in snippets.org
  • Shopping Items which get appended to the Shopping List in things.org
  • Media Entries (watch/read later items) that land in media.org

Org Capture Templates are explained here, Org Template expansion here.

;; Set org-capture inbox
(setq org-default-notes-file (concat org-directory "inbox.org"))
(define-key global-map "\C-cc" 'org-capture)

(setq things-file (expand-file-name "things.org" org-directory))
(setq media-file (expand-file-name "media.org" org-directory))

(defun get-domainname (address)
  "Extract TLD (without country) from ADDRESS.
Example: Return '200ok' from '[email protected]'."
  (replace-regexp-in-string
   "\-" "_"
   (nth 0
        (split-string (nth 1 (split-string address "@"))
                      "\\."))))

(defun from-name (fromname fromaddress from)
  "Return the first non-empty match for FROMNAME FROMADDRESS and FROM."
  (nth 0
       (seq-filter '(lambda (s)
                      (not (string-empty-p s)))
                   (list fromname fromaddress from))))

(setq org-capture-templates
      '(("t" "Todo" entry (file+olp things-file "Inbox" "Tasks")
         "* TODO %?\n  %U\n  %i\n  %a")
         ("w" "Waiting" entry (file+olp things-file "Waiting")
         "* WAITING %?\n  %U\n  %i\n  %a")
         ;; Creates an expense line for the date of the mail, prompts
         ;; for the amount and currency
         ("e" "Expense" table-line (file+olp things-file "Inbox" "Expenses")
          "|%(org-insert-time-stamp (org-read-date nil t \"%:date\") nil t) | %(from-name \"%:fromname\" \"%:fromaddress\" \"%:from\")| [[%:link][Mail]] | %^{amount} | %^{currency|usd|chf|eur} | |")
        ("m" "Mail" entry (file+olp things-file "Inbox" "Mails")
         ;; Creates "* TODO <2019-05-01 Wed> FromName [[mu4e:msgid:uuid][MessageSubject]] :200ok:
         ;; Therefore Emails can be properly:
         ;;   - Used as tasks
         ;;   - Attributed tags
         ;;   - Ordered by priority
         ;;   - Scheduled
         ;;   - etc
         "* TODO %(org-insert-time-stamp (org-read-date nil t \"%:date\") nil t) %(from-name \"%:fromname\" \"%:fromaddress\" \"%:from\") %a \t :%(get-domainname \"%:toaddress\"):")
        ("d" "Daily focus" plain (file+olp things-file "Inbox" "Daily")
         (file "~/.emacs.d/org-templates/daily_focus.org"))
        ("M" "Meeting minutes" plain (file+olp things-file "Inbox" "Tasks")
         (file "~/.emacs.d/org-templates/minutes.org"))
        ("s" "Code Snippet" entry (file+headline "~/src/200ok/knowledge/README.org" "Snippets")
         ;; Prompt for tag and language
         "* %?\t%^g\n#+BEGIN_SRC %^{language}\n%i\n#+END_SRC")
        ("S" "Shopping" entry (file+olp "~/Dropbox/org/shared_with_monika/shared_alain_and_monika.org" "Shopping")
         "* TODO %?\n  %U\n  %i\n  %a")
        ("p" "password" entry (file+headline "~/Dropbox/org/vault/primary.org.gpg" "Passwords")
         ;; Prompt for name
         "* %^{name}
 :PROPERTIES:
 :username: %^{username}
 :password: %(generate-password-non-interactive)
 :url: %^{url}

 :END:")
        ("u" "URL" entry
         (file+datetree media-file)
         "* %?\nURL: \nEntered on %U\n")))

Ensure text from capture-templates end with a newline

If they don’t, then the result will look like:

* Tasks
** TODO Foo from capture-template* This should be on the next line

This obviously breaks the structure of the Org file. Here’s a fix:

(defun add-newline-at-end-if-none ()
  "Add a newline at the end of the buffer if there isn't any."
  (save-excursion
    (save-restriction
      (goto-char (1- (point-max)))
      (if (not (looking-at "\n\n"))
          (progn
            (goto-char (point-max))
            (insert "\n"))))))

(add-hook 'org-capture-before-finalize-hook 'add-newline-at-end-if-none)

Structure templates

Enable the <s TAB syntax for structure templates.

(require 'org-tempo)

Pomodoro

A leightweight implementation of the Pomodoro Technique is implemented through customizing orgmode. For every Clock that is started (C-c C-x C-i) an automatic Timer is scheduled to 25min. After these 25min are up, a “Time to take a break!” message is played and a pop-up notification is shown.

The timer is not automatically stopped on clocking out, because clocking in should still work on new tasks without resetting the Pomodoro.

The timer can manually be stopped with M-x org-timer-stop.

A break can be started with M-x pomodoro-break. A pomodoro can also manually be started without clocking in via M-x pomodoro-start.

;; Configure primary org pomodoro buffer to which the timers will get
;; attached to.
(setq ok-pomodoro-buffer "things.org")
(load "~/.emacs.d/org-pomodoro")

Keyword sets

I use two workflow sets:

  • One for TODOs which can either be TODO or DONE
  • Another for tasks that I am WAITING for something to happen or which are in PROGRESS

Additionally I sometimes use the keywords PROJECT and AGENDA to denote special bullets that I might tag (schedule/deadline) in the agenda. These keywords give semantics to those bullets.

Note that “|” denotes a semantic state change that is reflected in a different color. Putting the pipe at the end means that all states prior should be shown in the same color.

(setq org-todo-keywords
          '((sequence "TODO" "|" "DONE")
            (sequence "PROJECT" "AGENDA" "|" "MINUTES")
            (sequence "WAITING" "|" "PROGRESS")))

Clock Table

Clocksum Format

When using a clock table, org will by default sum up the time in perfectly human readable terms like this:

HeadlineTime
Total time1d 1:03

For easy calculations (I don’t want to parse our hours, weeks and what not), I do prefer that the summation is done only in hours and minutes. Therefore, I over-wrote the org-time-clocksum-format function:

(setq org-duration-format 'h:mm)

This will render the same time as above as:

HeadlineTime
Total time25:03

GTD

(defun set-org-agenda-files ()
  "Set different org-files to be used in `org-agenda`."
  (setq org-agenda-files (list (concat org-directory "things.org")
                               (concat org-directory "reference.org")
                               (concat org-directory "media.org")
                               (concat org-directory "shared_with_monika/shared_alain_and_monika.org")
                               ;; "~/Dropbox/ZHAW/web3-unterlagen/README.org"
                               ;; "~/Dropbox/ZHAW/weng-unterlagen/README.org"
                               ;; "~/src/200ok/swiss-crowdfunder/TODO.org"
                               "~/src/200ok/200ok-admin/THINGS.org"
                               )))

(set-org-agenda-files)

(global-set-key "\C-cl" 'org-store-link)

(defun things ()
  "Open main 'org-mode' file and start 'org-agenda' for today."
  (interactive)
  (find-file (concat org-directory "things.org"))
  (set-org-agenda-files)
  (org-agenda-list)
  (org-agenda-day-view)
  (shrink-window-if-larger-than-buffer)
  (other-window 1))

Refile Targets

For a proficient GTD workflow, it is important to be able to refile one item from one list easily to another (for example when processing an inbox). Orgmode makes this easy with the refile command C-c C-w.

Define where the refiling can happen (the default is to the local buffer):

(setq org-refile-targets (quote ((org-agenda-files :maxlevel . 3))))

Show “calendar week” in calendar

(setq calendar-week-start-day 1)

(setq calendar-intermonth-text
      '(propertize
        (format "%2d"
                (car
                 (calendar-iso-from-absolute
                  (calendar-absolute-from-gregorian (list month day year)))))
        'font-lock-face 'font-lock-warning-face))

(setq calendar-intermonth-header
      (propertize "CW"
                  'font-lock-face 'font-lock-keyword-face))

Hide empty lines between sub-headers in collapsed view

(setq org-cycle-separator-lines 0)

Restclient mode

https://github.com/pashky/restclient.el

HTTP REST client tool for emacs

Integration into Org mode

https://github.com/alf/ob-restclient.el

An extension to restclient.el for emacs that provides org-babel support.

(org-babel-do-load-languages
 'org-babel-load-languages
 '((restclient . t)))

WIP Notifications / appointment reminders

This is a work-in-progress, but already working.

Important commands are:

  • M-x appt-check: At any point, re-display current appointment reminders
  • M-x apt-delete: Delete obsolete appointment reminders
    • Through Org, they only get added, but not deleted. Hence, when changing the reminder time, there will be two appointments in the queue.

https://orgmode.org/worg/org-faq.html#automatic-reminders

https://emacs.stackexchange.com/questions/3844/good-methods-for-setting-up-alarms-audio-visual-triggered-by-org-mode-events

https://orgmode.org/list/[email protected]/

;; Show first notification 2h before event
(setq appt-message-warning-time (* 60 2))
;; Then, have a reminder every 30min
(setq appt-display-interval 30)
;; Don't display the 'time to appointment in minutes' in the modeline
(setq appt-display-mode-line nil)

Option 1

;; ; Use appointment data from org-mode
;; (defun my-org-agenda-to-appt ()
;;   (interactive)
;;   (setq appt-time-msg-list nil)
;;   (org-agenda-to-appt))


;; ; Update alarms when...
;; ; (1) ... Starting Emacs
;; (my-org-agenda-to-appt)

;; ; (2) ... Everyday at 12:05am (useful in case you keep Emacs always on)
;; (run-at-time "12:05am" (* 24 3600) 'my-org-agenda-to-appt)

;; ; (3) ... When TODO.txt is saved
;; (add-hook 'after-save-hook
;;           '(lambda ()
;;              (if (string= (buffer-file-name) (concat (getenv "HOME") "/Dropbox/org/things.org"))
;;                  (my-org-agenda-to-appt))))

;; ; Display appointments as a window manager notification
;; (setq appt-disp-window-function 'my-appt-display)
;; (setq appt-delete-window-function (lambda () t))

;; (setq my-appt-notification-app (concat (getenv "HOME") "/bin/appt-notification"))

;; (defun my-appt-display (min-to-app new-time msg)
;;   (if (atom min-to-app)
;;     (start-process "my-appt-notification-app" nil my-appt-notification-app min-to-app msg)
;;   (dolist (i (number-sequence 0 (1- (length min-to-app))))
;;     (start-process "my-appt-notification-app" nil my-appt-notification-app (nth i min-to-app) (nth i msg)))))

Alternative:

(defadvice  org-agenda-redo (after org-agenda-redo-add-appts)
  "Pressing `r' on the agenda will also add appointments."
  (progn
    (setq appt-time-msg-list nil)
    (org-agenda-to-appt)))

(ad-activate 'org-agenda-redo)

(progn
  (appt-activate 1)
  (setq appt-display-format 'window)
  (setq appt-disp-window-function (function my-appt-disp-window))
  (defun my-appt-disp-window (min-to-app new-time msg)
    (call-process (concat (getenv "HOME") "/bin/appt-notification") nil 0 nil min-to-app msg new-time)))

(add-hook 'after-save-hook
          '(lambda ()
             (when (seq-contains org-agenda-files (s-replace "/home/munen" "~" (buffer-file-name)))
                 (org-agenda-to-appt))))

pdf-tools

https://github.com/politza/pdf-tools

PDF Tools is, among other things, a replacement of DocView for PDF files. The key difference is that pages are not pre-rendered by e.g. ghostscript and stored in the file-system, but rather created on-demand and stored in memory.

PDF Tools for me is - hands down - the best PDF viewer! It’s not an excuse to do even more within Emacs.

Configuration

When using evil-mode and pdf-tools and looking at a zoomed PDF, it will blink, because the cursor blinks. This configuration disables this whilst retaining the blinking cursor in other modes.

(evil-set-initial-state 'pdf-view-mode 'emacs)
(add-hook 'pdf-view-mode-hook
  (lambda ()
    (set (make-local-variable 'evil-emacs-state-cursor) (list nil))))

Elfeed

Elfeed is an extensible web feed reader for Emacs, supporting both Atom and RSS.

Configuration

(require 'elfeed)
(require 'elfeed-goodies)

(elfeed-goodies/setup)

Automatic word-wrap for elfeed entries:

(add-hook 'elfeed-show-mode-hook 'visual-line-mode)

Use VIM style scrolling in elfeed entries:

(define-key elfeed-show-mode-map (kbd "C-e") 'evil-scroll-line-down)
(define-key elfeed-show-mode-map (kbd "C-y") 'evil-scroll-line-up)

Define elfeed feeds

(load "~/.emacs.d/elfeed-feeds.el")

Mail

Writing and reading mail is inherently a text-based workflow. Yes, there’s HTML mails and attachments, but at the core Email is probably the place where many people write and consume the most text. To utilize the best text-processing program available makes a lot of sense.

When combined with other powerful features of Emacs (such as Org mode for organizing mails into projects and todos), processing mails within Emacs not only makes a lot of sense, but becomes a powerhouse.

mu4e

Emacs has many options for MTAs. I’m using MU4E which is a little similar to using mutt with notmuch. As SMTP, I’m using the built-in smtpmail Emacs package.

Installation

MU works on a local Maildir folder. For synchronization offlineimap is used. Install:

  • Debian: apt-get install offlineimap
  • macOS: brew install offlineimap

For MU4E to work, install MU and MU4E:

  • Debian: apt-get install mu4e
  • Guix: guix package -i mu
  • macOS: brew install mu --with-emacs

For starttls to work when sending mail, install gnutls:

  • Debian: apt-get install gnutls-bin
  • macOS: brew install gnutls

General

Authentication

Tell Emacs where to find the encrypted .authinfo file.

(setq auth-sources
    '((:source "~/.authinfo.gpg")))

PDFs

To open PDFs within Mu4e with Emacs, then there’s one thing to configure. Mu4e uses xdg-open to chose the app to open any mime type.

Configure xdg-open to use Emacs in .local/share/applications/mimeapps.list:

xdg-mime default emacs.desktop application/pdf

Configuration

mu setup (Initializing the message store):

https://www.djcbsoftware.nl/code/mu/mu4e/Initializing-the-message-store.html

Accounts setup

(require 'mu4e)

(require 'org-mu4e)

(setq send-mail-function 'smtpmail-send-it)

;; Default account on startup
(setq user-full-name  "Alain M. Lafon"
      mu4e-sent-folder "/200ok/INBOX.Sent"
      mu4e-drafts-folder "/200ok/INBOX.Drafts"
      mu4e-trash-folder "/200ok/INBOX.Trash")

(setq smtpmail-debug-info t
      message-kill-buffer-on-exit t
      ;; Custom script to run offlineimap in parallel for multiple
      ;; accounts as discussed here:
      ;; http://www.offlineimap.org/configuration/2016/01/29/why-i-m-not-using-maxconnctions.html
      ;; This halves the time for checking mails for 4 accounts for me
      ;; (when nothing has to be synched anyway)
      mu4e-get-mail-command "offlineimap_parallel.sh"
      mu4e-attachment-dir "~/Dropbox/org/files/inbox")

;; show full addresses in view message (instead of just names)
;; toggle per name with M-RET
(setq mu4e-view-show-addresses t)

;; Do not show related messages by default (toggle with =W= works
;; anyway)
(setq mu4e-headers-include-related nil)

;; Alternatives are the following, however in first tests they
;; show inferior results
;; (setq mu4e-html2text-command "textutil -stdin -format html -convert txt -stdout")
;; (setq mu4e-html2text-command "html2text -utf8 -width 72")
;; (setq mu4e-html2text-command "w3m -dump -T text/html")

(defvar my-mu4e-account-alist
  '(("200ok"
     (user-full-name  "Alain M. Lafon")
     (mu4e-compose-signature "200ok GmbH\nCEO\n\n[email protected]\n+41 76 405 05 67\nhttps://200ok.ch/\n\nCheck out our newest product: https://alephdam.200ok.ch/")
     (mu4e-compose-signature-auto-include t)
     (mu4e-sent-folder "/200ok/INBOX.Sent")
     (mu4e-drafts-folder "/200ok/INBOX.Drafts")
     (mu4e-trash-folder "/200ok/INBOX.Trash")
     (user-mail-address "[email protected]")
     (smtpmail-default-smtp-server "mail.your-server.de")
     (smtpmail-local-domain "200ok.ch")
     (smtpmail-smtp-user "[email protected]")
     (smtpmail-smtp-server "mail.your-server.de")
     (smtpmail-stream-type starttls)
     (smtpmail-smtp-service 25))
    ("zen-tempel"
     (user-full-name  "Zen Mönch Alain M. Lafon")
     (mu4e-compose-signature "Insopor Zen Akademie\nZen Mönch\n\n[email protected]\n+41 76 405 05 67\n\nhttps://zen-temple.net/")
     (mu4e-compose-signature-auto-include t)
     (mu4e-sent-folder "/zen-tempel/INBOX.Sent")
     (mu4e-drafts-folder "/zen-tempel/INBOX.Drafts")
     (mu4e-trash-folder "/zen-tempel/INBOX.Trash")
     (user-mail-address "[email protected]")
     (smtpmail-default-smtp-server "mail.your-server.de")
     (smtpmail-local-domain "zen-tempel.ch")
     (smtpmail-smtp-user "[email protected]")
     (smtpmail-smtp-server "mail.your-server.de")
     (smtpmail-stream-type starttls)
     (smtpmail-smtp-service 25))
    ("dispatched"
     (user-full-name  "Alain M. Lafon")
     (mu4e-compose-signature-auto-include nil)
     (mu4e-sent-folder "/dispatched/INBOX.Sent")
     (mu4e-drafts-folder "/dispatched/INBOX.Drafts")
     (mu4e-trash-folder "/dispatched/INBOX.Trash")
     (user-mail-address "[email protected]")
     (smtpmail-default-smtp-server "mail.your-server.de")
     (smtpmail-local-domain "dispatched.ch")
     (smtpmail-smtp-user "[email protected]")
     (smtpmail-smtp-server "mail.your-server.de")
     (smtpmail-stream-type starttls)
     (smtpmail-smtp-service 25))))

;; Whenever a new mail is to be composed, change all relevant
;; configuration variables to the respective account. This method is
;; taken from the MU4E documentation:
;; http://www.djcbsoftware.nl/code/mu/mu4e/Multiple-accounts.html#Multiple-accounts
(defun my-mu4e-set-account ()
  "Set the account for composing a message."
  (let* ((account
          (if mu4e-compose-parent-message
              (let ((maildir (mu4e-message-field mu4e-compose-parent-message :maildir)))
                (string-match "/\\(.*?\\)/" maildir)
                (match-string 1 maildir))
            (completing-read (format "Compose with account: (%s) "
                                     (mapconcat #'(lambda (var) (car var))
                                                my-mu4e-account-alist "/"))
                             (mapcar #'(lambda (var) (car var)) my-mu4e-account-alist)
                             nil t nil nil (caar my-mu4e-account-alist))))
         (account-vars (cdr (assoc account my-mu4e-account-alist))))
    (if account-vars
        (mapc #'(lambda (var)
                  (set (car var) (cadr var)))
              account-vars)
      (error "No email account found"))))

(add-hook 'mu4e-compose-pre-hook 'my-mu4e-set-account)

(add-hook 'mu4e-compose-mode-hook 'visual-line-mode)

(setq mu4e-refile-folder
      (lambda (msg)
        (cond
         ((string-match "^/dispatched.*"
                        (mu4e-message-field msg :maildir))
          "/dispatched/INBOX.Archive")
         ((string-match "^/zen-tempel.*"
                        (mu4e-message-field msg :maildir))
          "/zen-tempel/INBOX.Archive")
         ((string-match "^/200ok.*"
                        (mu4e-message-field msg :maildir))
          "/200ok/INBOX.Archive")
         ((string-match "^/zhaw.*"
                        (mu4e-message-field msg :maildir))
          "/zhaw/Archive")
         ;; everything else goes to /archive
         (t  "/archive"))))


;; Empty the initial bookmark list
(setq mu4e-bookmarks '())

;; Re-define all standard bookmarks to not include the spam folders
;; for searches
(defvar d-spam "NOT (maildir:/dispatched/INBOX.spambucket OR maildir:/zen-tempel/INBOX.spambucket OR maildir:/200ok/INBOX.spambucket OR maildir:/zhaw/\"Junk E-Mail\" OR maildir:/zhaw/\"Deleted Items\")")

;; All archived folders
(defvar d-archive "NOT (maildir:/dispatched/INBOX.Archive OR maildir:/zen-tempel/INBOX.Archive OR maildir:/200ok/INBOX.Archive OR maildir:/zhaw/Archive)")

(defvar inbox-folders (string-join '("maildir:/dispatched/INBOX"
                                     "maildir:/zhaw/INBOX"
                                     "maildir:/zen-tempel/INBOX"
                                     "maildir:/200ok/INBOX")
                                   " OR "))

(defvar draft-folders (string-join '("maildir:/dispatched/INBOX.Drafts"
                                     "maildir:/zhaw/Drafts"
                                     "maildir:/zen-tempel/INBOX.Drafts"
                                     "maildir:/200ok/INBOX.Drafts")
                                   " OR "))

(defvar spam-folders (string-join '("maildir:/dispatched/INBOX.spambucket"
                                     "maildir:/zhaw/INBOX.spambucket"
                                     "maildir:/zen-tempel/INBOX.spambucket"
                                     "maildir:/200ok/INBOX.spambucket")
                                  " OR "))

(add-to-list 'mu4e-bookmarks
             '((concat d-spam " AND date:today..now")                  "Today's messages"     ?t))
(add-to-list 'mu4e-bookmarks
             '((concat d-spam " AND date:7d..now")                     "Last 7 days"          ?w))
(add-to-list 'mu4e-bookmarks
             '((concat d-spam " AND flag:flagged")                     "Flagged"              ?f))
(add-to-list 'mu4e-bookmarks
             '((concat d-spam " AND mime:image/*")                     "Messages with images" ?p))
(add-to-list 'mu4e-bookmarks
             '(spam-folders "All spambuckets"     ?S))
(add-to-list 'mu4e-bookmarks
             '(draft-folders "All drafts"     ?d))
(add-to-list 'mu4e-bookmarks
             '(inbox-folders "All inbox mails"     ?i))
(add-to-list 'mu4e-bookmarks
             '((concat d-spam d-archive " AND (flag:unread OR flag:flagged) AND NOT flag:trashed")
               "Unread messages"      ?u))

Check for supposed attachments prior to sending them

(defvar my-message-attachment-regexp "\\(
                                      [Ww]e send\\|
                                      [Ii] send\\|
                                      attach\\|
                                      [aA]ngehängt\\|
                                      [aA]nhang\\|
                                      [sS]chicke\\|
                                      angehaengt\\|
                                      haenge\\|
                                      hänge\\)")
(defun my-message-check-attachment nil
  "Check if there is an attachment in the message if I claim it."
  (save-excursion
    (message-goto-body)
    (when (search-forward-regexp my-message-attachment-regexp nil t nil)
      (message-goto-body)
      (unless (or (search-forward "<#part" nil t nil)
                  (message-y-or-n-p
                   "No attachment. Send the message ?" nil nil))
        (error "No message sent")))))
(add-hook 'message-send-hook 'my-message-check-attachment)

For mail completion, only consider emails that have been seen in the last 6 months. This gets rid of legacy mail addresses of people.

(setq mu4e-compose-complete-only-after (format-time-string
                                        "%Y-%m-%d"
                                        (time-subtract (current-time) (days-to-time 150))))

HTML Mails

(require 'mu4e-contrib)
(setq mu4e-html2text-command 'mu4e-shr2text)
;;(setq mu4e-html2text-command "iconv -c -t utf-8 | pandoc -f html -t plain")
(add-to-list 'mu4e-view-actions '("ViewInBrowser" . mu4e-action-view-in-browser) t)

Disable “HTML over plain text” heuristic. This variable officially has this rationale: “Ratio between the length of the html and the plain text part below which mu4e will consider the plain text part to be ‘This messages requires html’ text bodies. You can neutralize it (always show the text version) by using `most-positive-fixnum’.”

This heuristic overwrites the default setting (and configuration) that Plain text should be preferred over HTML!

In my experience, HTML Emails are WAY longer than only 5x the Plain text (Doodle, Airbnb, Meetup, etc), so this will yield me a lot of false positives whereas I have never seen a “This message requires HTML” body.

I wrote an accompanying blog post with further information: https://200ok.ch/posts/2018-10-25_disable_mu4e_html_over_plain_text_heuristic.html

(setq mu4e-view-html-plaintext-ratio-heuristic  most-positive-fixnum)
(add-hook 'mu4e-compose-mode-hook 'flyspell-mode)

Updating mails:

  • Periodic - every 15 minutes
  • Happening in the background

Note: There’s no notifications, because that’s only distracting.

(setq mu4e-update-interval (* 15 60))
(setq mu4e-index-update-in-background t)

GPG configuration:

  • C-c RET s o to sign
  • C-c RET C-c to encrypt
  • C-c C-e v to verify the signature
  • C-c C-e d to decrypt

Always sign outgoing emails:

(setq mu4e-compose-crypto-reply-plain-policy 'sign)

When sending encrypted messages, also encrypt to self so that I can read the mail in the sent folder:

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

Don’t use gnus article view. Using it is an experimental feature of mu4e. Generally, the gnus article view is nice. But downloading attachments whilst having Swiper is not a good experience, because you’d have to type out the name of the saved file for every attachment. In the mu4e view, you can download each or all attachments without this additional step.

(setq mu4e-view-use-gnus nil)

With upgrading to Emacs 27, this broke mu4e-compose-crypto-reply-plain-policy set to 'sign. It always wanted to sign with s/mime whereas I want to sign with gpg. I think this option is obsolete. I’m leaving it here for a moment until I’m sure it will not be needed anymore.

;; (add-hook 'mu4e-compose-mode-hook 'epa-mail-mode)
;; (add-hook 'mu4e-view-mode-hook 'epa-mail-mode)

Automatic line breaks when reading mail

(add-hook 'mu4e-view-mode-hook 'visual-line-mode)

Do not reply to self

(setq mu4e-compose-dont-reply-to-self t)

Store link to message if in header view, not to header query.

(setq org-mu4e-link-query-in-headers-mode nil)

Customize header fields to show in mu4e-view. This only adds :bcc.

(setq mu4e-view-fields '(:from :to :cc :bcc :subject :flags :date :maildir :mailing-list :tags :attachments :signature :decryption))

Close mu4e without asking.

(setq mu4e-confirm-quit nil)

Reminder to keep to three sentences.

Rationale: E-mail takes too long to respond to, resulting in continuous inbox overflow for those who receive a lot of it.

http://three.sentenc.es/

(add-hook 'mu4e-compose-mode-hook
  (defun ok-mu4e-keep-to-three-sentences ()
    (shell-command "notify-send -u critical 'Keep to three sentences.'")
    (message "Keep to three sentences.")))

format=flowed

Setting format=flowed for non-text-based mail clients which don’t respect actual formatting, but let the text “flow” as they please. Relevant RFC: https://tools.ietf.org/html/rfc3676

What is required is a format which is in all significant ways Text/Plain, and therefore is quite suitable for display as Text/Plain, and yet allows the sender to express to the receiver which lines are quoted and which lines are considered a logical paragraph, and thus eligible to be flowed (wrapped and joined) as appropriate.

  • mu4e sets up visual-line-mode and also fill (M-q) to do the right thing.
  • each paragraph is a single long line; at sending, emacs will add the special line continuation characters.
(setq mu4e-compose-format-flowed t)

Some email clients ignore format=flowed (i.e. Outlook). Therefore, we send very long lines, so that they auto-flow. 998 chars are the actual maximum from the relevant RFC: https://www.ietf.org/rfc/rfc2822.txt

(setq fill-flowed-encode-column 998)

When looking at emails, show them nicely wrapped. That’s very helpful when people send mails with very long lines

(add-hook 'mu4e-view-mode-hook 'visual-line-mode)

Configure manually installed mu paths

(setq mu4e-msg2pdf "/usr/local/bin/msg2pdf")

Use Quoted printable text for outgoing messages to enable automatic line breaks

If this is successfull, send upstream PR to MU4E

https://mathiasbynens.be/notes/gmail-plain-text https://mothereff.in/quoted-printable https://www.gnu.org/software/emacs/manual/html_node/emacs-mime/qp.html

Mail filtering

Add a header action “Block” which add the Senders Name and From Address to a procmail blacklist.

(defun append-line-to-file (line path)
  "Append a `line` to a file behind `path`"
  (write-region (concat line "\n") nil path 'append))

(defun mu4e-blacklist-from (msg)
  "Add the `from` of a message to the procmail blacklist"
  (let* ((from (mu4e-message-field msg :from))
         (from_name (car (car from)))
         (from_address (cdr (car from))))
    ;; Block the senders Name
    (if from_name
        (append-line-to-file from_name "~/.procmail/blacklist_from.txt"))
    ;; Block the Email-Address
    (append-line-to-file from_address "~/.procmail/blacklist_from.txt")

    (message "Blocking: %s" from)))

(defun mu4e-blacklist-subject (msg)
  "Add the `subject` of a message to the procmail blacklist"
  (let* ((subject (mu4e-message-field msg :subject)))
    (if subject
        (append-line-to-file subject "~/.procmail/blacklist_subject.txt"))

    (message "Blocking: %s" subject)))

(add-to-list 'mu4e-headers-actions
  '("F Block 'From:'" . mu4e-blacklist-from) t)

(add-to-list 'mu4e-headers-actions
  '("S Block 'Subject:'" . mu4e-blacklist-subject) t)

Rewrite contact information

(defun munen-contact-processor (contact)
  (cond
   ((string-match "[email protected]" contact) ; Phil sometimes dosn't add his name to the reply-to.
    "Phil Hofmann <[email protected]>")
   ((string-match "[email protected]" contact) ; Monika writes her name in all caps and I don't want to forward it like that.
   "Monika Bieri <[email protected]>"
    )
   (t contact)))

(setq mu4e-contact-process-function 'munen-contact-processor)

Search / Completion

ido

ido means “Interactively Do Things”. ido has a completion engine that’s sensible to use everywhere. It is built-in and nice and could change a lot of defaults like find-file and switching buffers.

It works well while not breaking Emacs defaults.

(ido-mode t)
(ido-everywhere t)
(setq ido-enable-flex-matching t)

Ivy/Counsel/Swiper

https://github.com/abo-abo/swiper

Ivy, a generic completion mechanism for Emacs.

Counsel, a collection of Ivy-enhanced versions of common Emacs commands.

Swiper, an Ivy-enhanced alternative to isearch.

Ivy is an interactive interface for completion in Emacs. Therefore it overlaps in functionality with ido. While Ivy is more powerful, it breaks certain standard functionality. So ido is enabled globally by default and for certain tasks, Ivy overrides ido.

Emacs uses completion mechanism in a variety of contexts: code, menus, commands, variables, functions, etc. Completion entails listing, sorting, filtering, previewing, and applying actions on selected items. When active, ivy-mode completes the selection process by narrowing available choices while previewing in the minibuffer. Selecting the final candidate is either through simple keyboard character inputs or through powerful regular expressions.

Configuration

(setq enable-recursive-minibuffers t)
(global-set-key (kbd "<f6>") 'ivy-resume)

Show total amount of matches and the index of the current match

(setq ivy-count-format "(%d/%d) ")

Wrap to the first result when on the last result and vice versa.

(setq ivy-wrap t)

Enable Swiper

(global-set-key "\C-s" 'swiper)

Configure Counsel

(global-set-key (kbd "C-x b") 'counsel-ibuffer)
;; Run `counsel-ag` against the current directory and not against the
;; whole project
(global-set-key (kbd "C-c k") '(lambda()
                                 (interactive)
                                 (counsel-ag "" default-directory nil nil)))
(global-set-key (kbd "C-x l") 'counsel-locate)
(define-key minibuffer-local-map (kbd "C-r") 'counsel-minibuffer-history)

Override C-c C-j (org-goto) with counsel-org-goto which brings super fast fuzzy matching and navigation capabilities for headlines.

(define-key org-mode-map (kbd "C-c C-j") 'counsel-org-goto)

Override C-x i (insert-file) in favor of counsel-imenu which brings fuzzy matching to the ability to jump to any indexed position to the already great [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Imenu.html][imenu]].

(global-set-key (kbd "C-x i") 'counsel-imenu)

Override find-file and dired in favor of the counsel counter parts.

(global-set-key (kbd "C-x C-f") 'counsel-find-file)
(global-set-key (kbd "C-x d") 'counsel-dired)

Next to counsel, there’s also smex which is M-x combined with ido. smex has a better sorting algorithm than Counsel and having both installed means that we get the Counsel interface with smex sorting. Best of both worlds.

By default, counsel-M-x starts with a ^. More often than not, this will be in the way of me fuzzy matching a function. Therefore I’ll start it with an empty string as argument.

(global-set-key (kbd "M-x") (lambda ()
                              (interactive)
                              (counsel-M-x "")))

Where Ivy doesn’t work well

Overwriting standard Emacs functionality

Some basic features are overwritten when “everything” becomes an Ivy search buffer. For example:

  • When two dired buffers are open and files should be copied from one to the other, one can use the up and down keys to toggle the destination. When this is a search buffer, it will auto complete for all local folders, instead. Since copying files is something I do often, this already means I have to disable Ivy globally.
  • Tramp auto-completion doesn’t work for me. I’m using sudo:, ssh: and the likes a lot in dired mode. Auto completion when within Tramp is broken for me, so I always have to type out the whole connection string when Ivy is enabled for dired. Since this includes missing auto-completion on remote systems and such, it’s another valid reason to disable Ivy globally.

Disable Swiper where it is broken

Ivy/Swiper cannot search in PDFs. It tries to search in the PDF source code. Therefore I fall back to using isearch within PDFs.

(add-hook 'pdf-view-mode-hook '(lambda()
                                 (define-key pdf-view-mode-map "\C-s" 'isearch-forward)))

Improve other packages with ivy

Projectile completion (Default is ido)

(setq projectile-completion-system 'ivy)

Mu4e “folder” and “from” completion (Default is ido)

(setq mu4e-completing-read-function 'ivy-completing-read)

Synosaurus completion (Default is ido)

(setq synosaurus-choose-method 'ivy-read)

Obsolete alternatives

I used to use isearch instead of Swiper.

Replace i-search-(forward|backward) with their respective regexp capable counterparts

;;(global-set-key (kbd "C-s") 'isearch-forward-regexp)
;;(global-set-key (kbd "C-r") 'isearch-backward-regexp)

IRC

For chat-based communication, I like to use IRC. In my ~/.authinfo.gpg file, I have a line like:

machine irc.freenode.net login "munen" password SECRET_PASSWORD

This file is automatically read when connecting to servers. It’s the same for SMTP servers, for example.

For connecting to IRC, I’m using the built-in package erc.

Configure automatic join list

(setq erc-autojoin-channels-alist '(("freenode.net" "#200ok" "#emacsconf" "#emacsconf-org" "#lobsters")
                                    ;; This does not work, yet. The
                                    ;; channels cannot be joined on
                                    ;; connecting to bitlbee. bitlbee
                                    ;; needs to first connect to the
                                    ;; configured accounts (i.e.
                                    ;; slack). This could happen in a
                                    ;; timeout, or better event
                                    ;; oriented on a message that
                                    ;; bitlbee sends when connected to
                                    ;; the account.
                                    '(("localhost" "#internal" "#general"))))

Do not show join/quit info for lurkers

(setq erc-lurker-hide-list '("JOIN" "PART" "QUIT"))

Automatically unfold images when links are shared

(require 'erc-image)
(add-to-list 'erc-modules 'image)
(erc-update-modules)

Logging

(setq erc-log-channels-directory "~/.erc/logs/")
(add-hook 'erc-insert-post-hook 'erc-save-buffer-in-logs)

Notify when someone is addressing me

(setq erc-pals '("phi|" "branch14"))
;; The quotes around %s are super important to prevent shell injection
(add-hook 'erc-text-matched-hook '(lambda(match-type nickuserhost msg)
                                    (shell-command-to-string (format "notify-send erc '%s'" msg))))

Modeline

Spaceline

https://github.com/TheBB/spaceline

This part of the configuration was kindly provided by SirPscl.

(require 'spaceline)

Segments

Flycheck

Slightly simplified flycheck segments for info, warning and error.

(spaceline-define-segment ph/flycheck-warning-segment
  (if (flycheck-has-current-errors-p)
      (let ((c (cdr (assq 'warning (flycheck-count-errors
                                    flycheck-current-errors)))))
        (powerline-raw
         (if c (format "%s" c))))))

(spaceline-define-segment ph/flycheck-error-segment
  (if (flycheck-has-current-errors-p)
      (let ((c (cdr (assq 'error (flycheck-count-errors
                                    flycheck-current-errors)))))
        (powerline-raw
         (if c (format "%s" c))))))

(spaceline-define-segment ph/flycheck-info-segment
  (if (flycheck-has-current-errors-p)
      (let ((c (cdr (assq 'info (flycheck-count-errors
                                    flycheck-current-errors)))))
        (powerline-raw
         (if c (format "%s" c))))))

Default faces for the flycheck segments.

(defface ph/spaceline-flycheck-error-face
  '((t :inherit 'mode-line
       :weight bold
       :foreground "white"
       :background "dark red"))
  "Flycheck Error Face"
  :group 'spaceline)

(defface ph/spaceline-flycheck-warning-face
  '((t :inherit 'mode-line
       :weight bold
       :foreground "white"
       :background "DarkOrange3"))
  "Flycheck Warning Face"
  :group 'spaceline)

(defface ph/spaceline-flycheck-info-face
  '((t :inherit 'mode-line
       :weight bold
       :foreground "white"
       :background "dark green"))
  "Flycheck Info Face"
  :group 'spaceline)

Evil State

Setting the face according to evil-state.

(defun ph/spaceline-highlight-face-evil-state ()
  "Set the highlight face depending on the evil state."
  (if (bound-and-true-p evil-local-mode)
      (let* ((face (assq evil-state spaceline-evil-state-faces)))
        (if face (cdr face) (spaceline-highlight-face-default)))
    (spaceline-highlight-face-default)))

(setq-default spaceline-highlight-face-func
              'ph/spaceline-highlight-face-evil-state)

Set the evil-state segment colors for operator-state.

(defface ph/spaceline-evil-operator-face
  '((t (:background "cornflower blue"
        :inherit 'spaceline-evil-normal)))
  "Spaceline Evil Operator State"
  :group 'spaceline)

(add-to-list 'spaceline-evil-state-faces
             '(operator . ph/spaceline-evil-operator-face))

Git Branch

(defun ph/git-branch-name ()
  (replace-regexp-in-string "^ Git[:-]" "" vc-mode))

(spaceline-define-segment ph/version-control
  "Version control information."
  (when vc-mode
    (s-trim (concat (ph/git-branch-name)))))

Tramp

Tramp offers the following file name syntax to refer to files on other machines.

/method:host:filename
/method:user@host:filename
/method:user@host#port:filename

The following segemnts display the current buffer’s method and user@host.

(spaceline-define-segment ph/remote-method
  (when (and default-directory
             (file-remote-p default-directory 'method))
    (file-remote-p default-directory 'method)))

(spaceline-define-segment ph/remote-user-and-host
  (when (and default-directory
             (or
              (file-remote-p default-directory 'user)
              (file-remote-p default-directory 'host)))
    (concat
     (file-remote-p default-directory 'user) "@"
     (file-remote-p default-directory 'host))))

Default faces for the tramp segments.

(defface ph/spaceline-tramp-user-host-face
  '((t :inherit 'mode-line
       :foreground "black"
       :background "#fce94f"))
  "Tramp User@Host Face"
  :group 'spaceline)

(defface ph/spaceline-tramp-method-face
  '((t :inherit 'mode-line
       :foreground "black"
       :background "#ff5d17"))
  "Tramp Method Face"
  :group 'spaceline)

Mu4e Context

I’m not using Mu4e contexts, yet, because my configuration started before they were introduced. I’m leaving the segment configuration for the future.

;; (spaceline-define-segment ph/mu4e-context-segment
;;   (let ((context (mu4e-context-current)))
;;     (when (and context
;;                (string-prefix-p "mu4e" (symbol-name major-mode)))
;;       (mu4e-context-name context))))

Face for mu4e segemnt.

;; (defface ph/spaceline-mu4e-context-face
;;   '((t :inherit 'mode-line
;;        :weight bold))
;;   "mu4e face"
;;   :group 'spaceline)

Org Timer

I like to set timers, for example through org-pomodoro.el

(spaceline-define-segment org-timer-left-time
  "Show the time left in the current org-timer (i.e. a pomodoro)."
  (when (boundp 'org-timer-countdown-timer)
    (if org-timer-countdown-timer
      (let* ((rtime (decode-time
                     (time-subtract (timer--time org-timer-countdown-timer)
                                    (current-time))))
             (rsecs (nth 0 rtime))
             (rmins (nth 1 rtime))
             ;; Show time only in 15s increments (so it's not too
             ;; distracting). This could probably done in math instead
             ;; of a cond statement.
             (dsecs (cond
                     ((>= rsecs 45) 45)
                     ((>= rsecs 30) 30)
                     ((>= rsecs 15) 15)
                     ;; Usually round seconds down to 0.
                     ((and
                       (< rsecs 15)
                       (> rmins 0)) 0)
                     ;; Unless it's less than 15 secs left, then
                     ;; actually count down.
                     ((and
                       (< rsecs 15)
                       (= rmins 0)) rsecs))))
        (format "%02d:%02d" rmins dsecs)))))

Setup

Setting up the mode-line and order of segements. Compile the modeline with M-x spaceline-compile.

(require 'spaceline-config)
;; Otherwise spaceline will be huge in Emacs >= 27.1
(setq powerline-height 1)
;; Since Emacs >= 27.1, there's no need for font trickery. Just use
;; UTF-8.
(setq powerline-default-separator 'utf-8)

(spaceline-emacs-theme)
(spaceline-install
  'main
  '((evil-state :face highlight-face)
    (buffer-id)
    (org-timer-left-time)
    ;; (ph/mu4e-context-segment :face 'ph/spaceline-mu4e-context-face)
    (ph/remote-method :face 'ph/spaceline-tramp-method-face)
    (ph/remote-user-and-host :face 'ph/spaceline-tramp-user-host-face)
    (buffer-modified))
  '(;;(minor-modes :when active)
    (projectile-root)
    (ph/version-control)
    ;(line-column :when active)
    ;(buffer-position :when active)
    (ph/flycheck-info-segment :face 'ph/spaceline-flycheck-info-face :when active)
    (ph/flycheck-warning-segment :face 'ph/spaceline-flycheck-warning-face :when active)
    (ph/flycheck-error-segment :face 'ph/spaceline-flycheck-error-face :when active)
    (line-column)
    (major-mode)))

Set mode-line always active (don’t hide segments when focus is on a different window).

(defun powerline-selected-window-active () t)

Diminish

Diminish implements hiding or abbreviation of the mode line displays (lighters) of minor-modes.

(eval-after-load "auto-revert"
  '(diminish 'auto-revert-mode))
(eval-after-load "beacon"
  '(diminish 'beacon-mode))
(eval-after-load "ivy"
  '(diminish 'ivy-mode))
(eval-after-load "projectile"
  '(diminish 'projectile-mode))
(eval-after-load "projectile-rails"
  '(diminish 'projectile-rails-mode))
(eval-after-load "rainbow-mode"
  '(diminish 'rainbow-mode))
(eval-after-load "undo-tree"
  '(diminish 'undo-tree-mode))
(eval-after-load "which-key"
  '(diminish 'which-key-mode))

hide-mode-line

https://github.com/hlissner/emacs-hide-mode-line

A minor mode that hides (or masks) the mode-line in your current buffer. It can be used to toggle an alternative mode-line, toggle its visibility, or simply disable the mode-line in buffers where it isn’t very useful otherwise.

(require 'hide-mode-line)

(add-hook 'pdf-view-mode-hook #'hide-mode-line-mode)

Write Quality

writegood-mode

https://github.com/bnbeckwith/writegood-mode

This is a minor mode to aid in finding common writing problems.

It highlights text based on a set of weasel-words, passive-voice and duplicate words.

Theraurus

https://github.com/hpdeifel/synosaurus/

Synosaurus is a thesaurus front-end with pluggable back-end.

Use the openthesaurus.de back-end.

(setq synosaurus-backend 'synosaurus-backend-openthesaurus)

(defalias 'thesaurus-openthesaurus-de 'synosaurus-lookup)

Flyspell

Emacs has built-in functionality for checking and correcting spelling called ispell.el. On top of that, there’s a built-in minor mode for for on-the-fly spell checking. called flyspell-mode.

Flyspell can use multiple back-ends (for example ispell, aspell or hunspell).

Configuration

Order corrections by likeliness

Do not order not by the default of alphabetical ordering.

(setq flyspell-sort-corrections nil)

Do not print messages for every word

When checking the entire buffer, don’t print messages for every word. This is a major performance gain.

(setq flyspell-issue-message-flag nil)

Use hunspell with multiple dictionaries

Here in Switzerland, there are four official languages: Swiss German, French, Italian and Romansh. Also, we converse a lot in German and English. Hence, it’s a regular occurrence to have one file with multiple languages in them. Especially for these situations it’s still to have proper spell checking. Fortunately, Emacs has us covered!

Hunspell is a free spell checker and used by LibreOffice, Firefox and Chromium. It allows to set multiple dictionaries - even different dictionaries per language (aspell, for example also allows multiple dictionaries, but only for the same language). It also can be used as an ispell.el backend.

To use hunspell, install it first:

apt install hunspell hunspell-de-de hunspell-en-gb hunspell-en-us hunspell-de-ch-frami
(with-eval-after-load "ispell"
  ;; Configure `LANG`, otherwise ispell.el cannot find a 'default
  ;; dictionary' even though multiple dictionaries will be configured
  ;; in next line.
  (setenv "LANG" "en_US")
  (setq ispell-program-name "hunspell")
  ;; Configure German, Swiss German, and two variants of English.
  (setq ispell-dictionary "de_DE,de_CH,en_GB,en_US")
  ;; ispell-set-spellchecker-params has to be called
  ;; before ispell-hunspell-add-multi-dic will work
  (ispell-set-spellchecker-params)
  (ispell-hunspell-add-multi-dic "de_DE,de_CH,en_GB,en_US")
  ;; For saving words to the personal dictionary, don't infer it from
  ;; the locale, otherwise it would save to ~/.hunspell_de_DE.
  (setq ispell-personal-dictionary "~/.hunspell_personal"))

;; The personal dictionary file has to exist, otherwise hunspell will
;; silently not use it.
(unless (file-exists-p ispell-personal-dictionary)
        (write-region "" nil ispell-personal-dictionary nil 0))

Do not loose all spellchecking information after adding one word to a personal dictionary

Advice to re-check the buffer after a word has been added to the dictionary. This has the benefit of the word actually being cleared, but the downside that the whole buffer has to be re-checked which an take some time.

;; (defun flyspell-buffer-after-pdict-save (&rest _)
;;   (flyspell-buffer))

;; (advice-add 'ispell-pdict-save :after #'flyspell-buffer-after-pdict-save)

The proper solution (for which I don’t have time now) is to just mark all further occurrences of the word you just saved as correct (without having to recheck the whole buffer).

Deprecated aspell setup

Alternatively to using hunspell, here’s an option to switch the dictionary between German and English.

The German dictionary is from here.

;; (defun flyspell-switch-dictionary()
;;   "Switch between German and English dictionaries"
;;   (interactive)
;;   (let* ((dic ispell-current-dictionary)
;;          (change (if (string= dic "deutsch") "english" "deutsch")))
;;     (ispell-change-dictionary change)
;;     (message "Dictionary switched from %s to %s" dic change)))

Implement ispell-pdict-save with above requirement

OS Specific

Linux

“Fira Code Retina” as default font. Get it via the fonts-firacode Debian package.

(when (eq system-type 'gnu/linux)
      (add-to-list 'default-frame-alist
                   '(font . "Fira Code Retina 9"))
      ;; Manually setting the font
      ;; (set-frame-font "Fira Code Retina 9")
  ;; Default Browser
  (setq browse-url-browser-function 'browse-url-generic
        browse-url-generic-program "firefox"
        browse-url-new-window-flag t)
  (menu-bar-mode -1)
  ;; enable pdf-tools
  (pdf-tools-install))

Display Emoji (requires the fonts-symbola Debian package)

(set-fontset-font t nil "Symbola" nil 'prepend)

macOS

(when (eq system-type 'darwin)
  (set-frame-font "Menlo 14")
  ; Use Spotlight to search with M-x locate
  (setq locate-command "mdfind"))

Presentation / Beamer

Set safe themes (to execute LISP code)

(setq custom-safe-themes
   (quote
    ("df3e05e16180d77732ceab47a43f2fcdb099714c1c47e91e8089d2fcf5882ea3"
     "d09467d742f713443c7699a546c0300db1a75fed347e09e3f178ab2f3aa2c617"
     "8db4b03b9ae654d4a57804286eb3e332725c84d7cdab38463cb6b97d5762ad26"
     "85c59044bd46f4a0deedc8315ffe23aa46d2a967a81750360fb8600b53519b8a"
     default)))

Configure dark-mode theme and font size

(defun dark-mode ()
  "Default theme and font size.  Pendant: (presentation-mode)."
  (interactive)

  (mapcar 'disable-theme custom-enabled-themes)
  (set-face-attribute 'default nil :height 150)
  ;; Themes
  ;; (set-frame-parameter nil 'background-mode 'dark)

  ;; Dark, High Contrast <- favorite
  (load-theme 'wombat)
  (setq frame-background-mode (quote dark))

  ;; Dark, Low contrast
  ;; (load-theme 'darktooth)
  ;; Dark, Lowest contrast
  ;; (load-theme 'zenburn)
   )

Configure light-mode theme and font size

(defun light-mode ()
  "Enables a light theme."
  (interactive)
  (set-face-attribute 'default nil :height 100)
  (mapcar 'disable-theme custom-enabled-themes)
  (load-theme 'spacemacs-light t))
(defun presentation-mode ()
  "Presentation friendly theme and font size."
  (interactive)
  (load-theme 'leuven t)
  (mapcar 'disable-theme custom-enabled-themes)
  (set-face-attribute 'default nil :height 150))

Enable default theme and font

(light-mode)

Org Mode Exports

Time Export Table

Create a customized time table ready for CSV export.

Usage:

#+name: ok-timetable
#+BEGIN_SRC elisp
(ok-export-org-timetable "2018-05-09")
#+END_SRC

When evaluating the src-block above, it’ll yield a table like:

#+RESULTS: ok-timetable
|       date |  hours | task                             |
|------------+--------+----------------------------------|
| 2018-05-09 |   0:02 | #support                         |
| 2018-05-09 |   0:17 | #support                         |
|------------+--------+----------------------------------|

(require 'seq)

(defun ok-filter-table-by-date (tbl from-date table-row)
  "Filter a TBL by FROM-DATE which is found in TABLE-ROW."
  ;; Sort by date
  (seq-sort '(lambda (e1 e2)
               (string-lessp (nth table-row e1)
                             (nth table-row e2)))
            ;; Filter to start with FROM-DATE
            (seq-filter (lambda (elem)
                          (let ((date-elem (nth table-row elem)))
                            ;; >=
                            (when (or (string-greaterp date-elem from-date)
                                      (string-equal date-elem from-date))
                              elem)))
                        tbl)))

(defun ok-hm-to-hours (worktime)
  "Casts HH:MM WORKTIME into a floating point number."
  (condition-case worktime
      (let* ((time (split-string worktime ":"))
             (minutes (/ (string-to-number (second time))
                         60.0))
             (hours (string-to-number (first time))))
        (format "%.3f" (+ hours minutes)))
    (error 0)))

(defun ok-split-hash-and-description (text)
  "Given a TEXT like '#tag1 #tag2 some description' and return tags and description as a list."
  ;; The concat is a little hack, so that there's always a minimum
  ;; description to be found
  (let ((text (concat text " ")))
    ;; A hashtag can have numbers, dashes and a-z
    (if (string-match "\\(#[a-z-0-9]+ \\)+" text)
        (let* ((hashtags (match-string 0 text))
               ;; Couldn't figure out how to get the description
               ;; through an elisp regexp, so I'm just reading the
               ;; remainder of the text after all hashtags here
               (description (substring text (length hashtags) (length text))))
          (list
           (string-trim hashtags)
           (string-trim description))))))

(defun ok-find-parent-of-type (elem-type elem)
  "For a child ELEM, find the closes parent element of type ELEM-TYPE."
  (let ((parent-elem (org-element-property :parent elem)))
    (if (eq elem-type (car parent-elem))
        parent-elem
      (ok-find-parent-of-type elem-type parent-elem))))

(defun ok-generate-clock-table ()
  "Generate a list of org elements of type 'clock."
  (let* ((ast (org-element-parse-buffer 'element)))
    ;; Map a function to all elements of TYPE 'clock which extracts
    ;; the TITLE, DURATION and DATE of a TODO.
    (org-element-map ast 'clock
      (lambda (clock-elem)
        (let* ((val (org-element-property :value clock-elem))
               (task (ok-find-parent-of-type 'headline clock-elem))
               (hash-and-description (ok-split-hash-and-description
                                      (org-element-property :title task))))
          `(,(let ((year (org-element-property :year-start val))
                   (month (org-element-property :month-start val))
                   (day (org-element-property :day-start val)))
               (format "%4d-%02d-%02d" year month day ))
            ,(ok-hm-to-hours (org-element-property :duration clock-elem))
            ,(first hash-and-description)
            ,(second hash-and-description)))))))

(defun ok-export-org-timetable (from-date)
  "Generate a list from 'org-mode' clock elements starting from FROM-DATE."
  ;; Concatenate header, element data and footer into one list which
  ;; will automatically be rendered by org-mode as a table.
  (append
   '(("date" "duration" "hashtags" "description"))
   '(hline)
   ;; Generate tree of all visible elements within buffer (narrowing
   ;; works).
   (ok-filter-table-by-date (ok-generate-clock-table) from-date 0)))


(defun ok-export-table-to (table-name target-path)
  "Exports the contents of a table called TABLE-NAME to a CSV file at TARGET-PATH."
  (progn
    (save-excursion
      (goto-char (point-min))
      (search-forward (concat "RESULTS: " table-name))
      (org-cycle)
      (evil-next-line)
      (org-table-export target-path "orgtbl-to-csv")
      (evil-previous-line))
      (org-cycle))
    (message "Table export completed"))

Export Org Mode TODO headers into estimation table

(load-file "~/src/200ok/200ok-admin/src/export-org-estimations/ok-export-org-estimations.el")

General config

Don’t export the html-validation-link.

(setq org-html-validation-link nil)

Bad experience

The following packages would be nice, in theory. In practice something is yet amiss, but it might be different in the future. That’s why I’m keeping them around and will try them at another time.

clipmon

https://github.com/bburns/clipmon

Proposition: Monitors system clipboard and puts everything in the kill-ring.

Caveat: In theory, I liked the package. However, it seemed to cause racing conditions and crashed Emacs multiple times a day. When this is re-implemented in a non-blocking mode, this would be nice.

;; (add-to-list 'after-init-hook 'clipmon-mode-start)

Comment

Theoretically this is really nice to have functionality. However, I couldn’t run it for long. Emacs started freezing a lot on the day when I added this lib. I assume, because clipmon is blocking - and I always run multiple instances of Emacs in parallel. They might be in for a classic racing condition. Might be just another bug.

parinfer-mode

Proposition: When working with Lisp, there’s the option of handing parentheses manually or let them be dealt with by the magic that is Parinfer. I’m using the wonderful parinfer-mode.

Caveat: The original Parinfer curiously is written in JavaScript. parinfer-mode is a re-implementation in Elisp. When I tried it, it was still in it’s early stages and quite buggy. However, the original Parinfer algorithm is quite nice. I’ll try again at some point.

;; (add-hook 'clojure-mode-hook #'parinfer-mode)
;; (add-hook 'emacs-lisp-mode-hook #'parinfer-mode)
;; (setq parinfer-extensions '(company pretty-parens evil))
;; (eval-after-load "parinfer"
;;   '(progn
;;      (define-key parinfer-mode-map (kbd "C-,") 'parinfer-toggle-mode)
;;      (define-key parinfer-region-mode-map (kbd ">") 'parinfer-shift-right)
;;      (define-key parinfer-region-mode-map (kbd "<") 'parinfer-shift-left)))

Other good Emacs configurations

Modes to check out

Modes I probably could use, but haven’t tried out, yet.

rspec-mode

Kudos SirPscl

https://github.com/pezra/rspec-mode#usage

Increase selected region by semantic units

https://github.com/magnars/expand-region.el

About

My emacs configuration documented in literate programming style

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Emacs Lisp 100.0%