You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

49 KiB

Emacs Configuration

Initialization

Lexical scope

;;; init.el --- user-init-file                    -*- lexical-binding: t -*-
;;;
;;; DO NOT EDIT THIS FILE!
;;; This file was automatically generated by tangling `init.org`.

Package management

I use Nix with the emacs overlay to build Emacs including packages directly from use-package declarations in this file. The packages are already added to load-path, so we can disable Emacs' own package manager entirely.

Disable Emacs' own package manager

(require 'package)
(setq package-archives nil
      package-enable-at-startup nil)

TODO (Probably) move to early-init.el in Emacs 27

Install packages and add them to the current session's load-path

A rebuild of my Emacs Nix expression will not alter the current session's load-path. The functions here are quick and dirty ways to load a package in the current session, without having to add Nix' store paths to load-path manually.

(defun dkellner/add-elpa-package-to-load-path (package)
  "Install PACKAGE from ELPA and add Nix' store path to `load-path'.

This does install dependencies but does not (yet) add them to
`load-path'. You need to call this function manually for any
missing dependencies."
  (interactive "sPackage: ")
  (dkellner/add-package-to-load-path
   "nixpkgs.emacsPackages.elpaPackages"
   package))

(defun dkellner/add-melpa-package-to-load-path (package)
  "Install PACKAGE from MELPA and add Nix' store path to `load-path'.

This does install dependencies but does not (yet) add them to
`load-path'. You need to call this function manually for any
missing dependencies."
  (interactive "sPackage: ")
  (dkellner/add-package-to-load-path
   "nixpkgs.emacsPackages.melpaPackages"
   package))

;; TODO: error handling
(defun dkellner/add-package-to-load-path (packageSet package)
  (let* ((nix-expr (format "%s.%s" packageSet package))
         (build-output (shell-command-to-string
                        (format "nix build --no-link %s" nix-expr)))
         (root (shell-command-to-string
                (format "nix eval --raw %s" nix-expr)))
         (dir-regex (format "%s.*" package))
         (path (car (directory-files
                     (concat root "/share/emacs/site-lisp/elpa/")
                     t dir-regex))))
    (add-to-list 'load-path path)))

Common

These packages and functions are used by various other subsections.

use-package

(require 'use-package)

no-littering

no-littering needs to be loaded as early as possible, see https://github.com/emacscollective/no-littering#usage for details.

(use-package no-littering
  :ensure t)

diminish

(use-package diminish
  :ensure t)

hydra

(use-package hydra
  :ensure t)

Defining global keybindings for EXWM

(defun dkellner/exwm-bind-keys (&rest bindings)
  "Like exwm-input-set-key but syntax similar to bind-keys.

Define keybindings that work in exwm and non-exwm buffers.
Only works *before* exwm in initialized."
(pcase-dolist (`(,key . ,fun) bindings)
  (add-to-list 'exwm-input-global-keys `(,(kbd key) . ,fun))))

Sensible defaults

A good starting point: `better-defaults`

From https://github.com/technomancy/better-defaults : "[…] this package attempts to address the most obvious of deficiencies in uncontroversial ways that nearly everyone can agree upon."

(use-package better-defaults
  :ensure t
  :config
  (ido-mode -1))  ; I prefer ivy-mode

Store customizations in a separate file

(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(when (file-exists-p custom-file)
  (load custom-file))

Remove trailing whitespace on save

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

Kill the current buffer without confirmation

(bind-key "C-x k" #'dkellner/kill-current-buffer)

(defun dkellner/kill-current-buffer ()
  "Kill the current buffer."
  (interactive)
  (kill-buffer (current-buffer)))

Enable some commands that are disabled by default

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

Unify the way Emacs is asking for confirmation

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

EXWM

EXWM is a tiling window manager for Emacs. Each X window will get its own Emacs buffer that you can switch to, split, close etc. like any other buffer.

The only X program I run often enough to care about efficiency is Firefox. To integrate nicely with EXWM I disabled tabs in my browser, so each open website will have its own buffer that I can conveniently switch to by fuzzy finding in ivy-switch-buffer.

Start EXWM via .xinitrc

xhost +SI:localuser:$USER

if [[ $(xrandr | grep "HDMI-2 connected") ]]; then
    xrandr --dpi 96
else
    xrandr --dpi 192
fi
autorandr -c

xset b off
xset s off
xset -dpms
xsetroot -cursor_name left_ptr
xrdb -merge ~/.Xresources

setxkbmap de neo

xscreensaver -no-splash &
picom &
dunst &
cbatticon &
nm-applet &

$HOME/hacks/git-autocommit.sh "$HOME/org/"

feh --bg-fill "/home/dkellner/Pictures/wallpapers/Penrose1920x1080.jpg"

# emacs --daemon
exec emacsclient -c
# exec dbus-launch --exit-with-session emacs

Configuration

(use-package exwm
  :ensure t
  :demand t
  :config
  (setq exwm-workspace-number 10
        exwm-workspace-show-all-buffers t
        exwm-layout-show-all-buffers t)

  (dotimes (i 10)
    (exwm-input-set-key (kbd (format "s-%d" i))
                        `(lambda ()
                           (interactive)
                           (exwm-workspace-switch-create ,i))))

  (dkellner/exwm-bind-keys
   '("s-b" . ivy-switch-buffer)
   '("s-q" . exwm-reset)
   '("s-f" . dkellner/browse/body)
   '("s-i" . exwm-input-toggle-keyboard)
   '("s-R" . (lambda () (interactive) (async-shell-command "autorandr -c")))
   '("s-Z" . (lambda () (interactive) (async-shell-command "xscreensaver-command -lock")))
   '("s-Q" . dkellner/shutdown-or-reboot/body))

  (setq exwm-input-simulation-keys
        '(([?\M-<] . [home])
          ([?\M->] . [end])
          ([?\C-k] . [S-end ?\C-x])
          ([?\C-w] . [?\C-x])
          ([?\C-s] . [?\C-f])
          ([?\C-g] . [esc])
          ([?\C-x ?\C-s] . [?\C-s])
          ([?\M-w] . [?\C-c])
          ([?\C-y] . [?\C-v])))

  (add-hook 'exwm-update-class-hook #'dkellner/exwm-update-class-hook)
  (add-hook 'exwm-update-title-hook #'dkellner/exwm-update-title-hook)

  ;; see https://github.com/ch11ng/exwm/wiki/EXWM-User-Guide#an-issue-with-ediff
  (setq ediff-window-setup-function 'ediff-setup-windows-plain)

  (require 'exwm-randr)
  (setq exwm-randr-workspace-monitor-plist '(6 "HDMI-2" 7 "HDMI-2"
                                             8 "HDMI-2" 9 "HDMI-2"
                                             0 "HDMI-2"))
  (exwm-randr-enable)

  (require 'exwm-systemtray)
  (exwm-systemtray-enable)

  (exwm-enable))

(defun dkellner/exwm-update-class-hook ()
  (unless (dkellner/exwm-use-title-for-buffer-name)
    (exwm-workspace-rename-buffer exwm-class-name)))

(defun dkellner/exwm-update-title-hook ()
  (when (or (not exwm-instance-name)
            (dkellner/exwm-use-title-for-buffer-name))
    (exwm-workspace-rename-buffer exwm-title)))

(defun dkellner/exwm-use-title-for-buffer-name ()
  (or (string-prefix-p "sun-awt-X11-" exwm-instance-name)
      (string= "gimp" exwm-instance-name)
      (string-prefix-p "puzzleandplay.whereby.com" exwm-instance-name)
      (string= "qutebrowser" exwm-instance-name)
      (string= "Navigator" exwm-instance-name)))

Use M-y in EXWM buffers

(defun dkellner/exwm-counsel-yank-pop ()
  "Same as `counsel-yank-pop' and paste into exwm buffer.

Source: https://github.com/DamienCassou/gpastel"
  (interactive)
  (let ((inhibit-read-only t)
        ;; Make sure we send selected yank-pop candidate to
        ;; clipboard:
        (yank-pop-change-selection t))
    (call-interactively #'counsel-yank-pop))
  (when (derived-mode-p 'exwm-mode)
    ;; https://github.com/ch11ng/exwm/issues/413#issuecomment-386858496
    (exwm-input--set-focus (exwm--buffer->id (window-buffer (selected-window))))
    (exwm-input--fake-key ?\C-v)))

(bind-key "M-y" #'dkellner/exwm-counsel-yank-pop exwm-mode-map)

Window navigation

(dkellner/exwm-bind-keys
 '("s-n" . windmove-left)
 '("s-t" . windmove-right)
 '("s-g" . windmove-up)
 '("s-r" . windmove-down)
 '("s-." . split-window-right)
 '("s-," . split-window-below)
 '("s-m" . delete-other-windows)
 '("s-j" . delete-window)
 '("s-N" . (lambda () (interactive) (shrink-window-horizontally 2)))
 '("s-T" . (lambda () (interactive) (enlarge-window-horizontally 2))))

Prevent suspending

Suspending Emacs causes EXWM to freeze. You can recover by sending SIGUSR2 to the running emacsclient process, but that is rather cumbersome.

(global-unset-key (kbd "C-z"))
(global-unset-key (kbd "C-x C-z"))

Desktop environment

These are typical responsibilities of a desktop environment. We'll teach Emacs how to handle those.

Brightness and volume control

At the moment these shell out to some simple scripts I've been using for years, basically just wrapping light and pactl.

(dkellner/exwm-bind-keys
 '("<XF86MonBrightnessUp>" . (lambda () (interactive)
                               (async-shell-command "~/hacks/brightnessctl.sh inc")))
 '("<XF86MonBrightnessDown>" . (lambda () (interactive)
                                 (async-shell-command "~/hacks/brightnessctl.sh dec")))
 '("<XF86AudioRaiseVolume>" . (lambda () (interactive)
                                (async-shell-command "~/hacks/volumectl.sh inc")))
 '("<XF86AudioLowerVolume>" . (lambda () (interactive)
                                (async-shell-command "~/hacks/volumectl.sh dec")))
 '("<XF86AudioMute>" . (lambda () (interactive)
                         (async-shell-command "~/hacks/volumectl.sh toggle")))
 '("<XF86AudioMicMute>" . (lambda () (interactive)
                            (async-shell-command "~/hacks/volumectl.sh mic_toggle"))))

Clipboard management

(use-package gpastel
  :load-path "~/dev/gpastel"
  :hook (exwm-init . gpastel-mode))

Shutdown and reboot

Simply running shutdown -h now in a terminal will cause Emacs to not shutdown properly. For example, the list of recently used files will not be persisted.

dkellner/prepare-kill-and-run solves this by placing the actual shutdown command at the end of kill-emacs-hook. This way it is executed just before Emacs would exit normally.

(defhydra dkellner/shutdown-or-reboot (:exit t)
  "Shutdown/reboot?"
  ("s" #'dkellner/shutdown "shutdown")
  ("r" #'dkellner/reboot "reboot"))

(defun dkellner/shutdown ()
  "Kills emacs properly and shutdown."
  (interactive)
  (dkellner/prepare-kill-and-run "shutdown -h now"))

(defun dkellner/reboot ()
  "Kill emacs properly and reboot."
  (interactive)
  (dkellner/prepare-kill-and-run "shutdown -r now"))

(defun dkellner/prepare-kill-and-run (command)
  "Prepare to kill Emacs properly and execute COMMAND.

This allows us to shutting down or rebooting the whole system and still
saving recently used files, bookmarks, places etc."
  (when (org-clock-is-active)
    (org-clock-out))
  (let ((kill-emacs-hook (append (remove #'server-force-stop kill-emacs-hook)
                                 (list (lambda () (shell-command command))))))
    (save-buffers-kill-emacs)))

Shutdown on critical battery level

(use-package battery
  :defer 10
  :config
  (defun dkellner/shutdown-on-critical-battery ()
    "Ask to call `dkellner/shutdown' if the battery level is below 10%."
    (let* ((battery-status (funcall battery-status-function))
           (ac-line-status (cdr (assq ?L battery-status)))
           (load-percentage (string-to-number (cdr (assq ?p battery-status)))))
      (when (and (string-equal ac-line-status "BAT")
                 (< load-percentage 10.0)
                 (y-or-n-p-with-timeout "Battery level critical. Shutdown?" 30 t))
        (dkellner/shutdown))))

  (defvar dkellner/shutdown-on-critical-battery-timer nil)
  (unless (timerp dkellner/shutdown-on-critical-battery-timer)
    (setq dkellner/shutdown-on-critical-battery-timer
          (run-at-time t 60 #'dkellner/shutdown-on-critical-battery))))

Running certain applications directly from M-x

These are basically just "shortcut functions" so I can type the name of the application I want to run directly in M-x.

(defun dkellner/tor-browser ()
  (interactive)
  (async-shell-command "tor-browser"))

(defun dkellner/firefox ()
  (interactive)
  (async-shell-command "firefox"))

(defun dkellner/chromium ()
  (interactive)
  (async-shell-command "chromium"))

(defun dkellner/profanity ()
  (interactive)
  (dkellner/vterm-command "profanity"))

(defun dkellner/alsamixer ()
  (interactive)
  (dkellner/vterm-command "alsamixer"))

(defun dkellner/pavucontrol ()
  (interactive)
  (async-shell-command "pavucontrol"))

(defun dkellner/mutt ()
  (interactive)
  (dkellner/vterm-command "mutt"))

(defun dkellner/element ()
  (interactive)
  (async-shell-command "element-desktop"))

(defun dkellner/signal ()
  (interactive)
  (async-shell-command "signal-desktop"))

(defun dkellner/slack ()
  (interactive)
  (async-shell-command "slack"))

(defun dkellner/discord ()
  (interactive)
  (async-shell-command "Discord"))

TODO Write a macro

Navigation and editing

Boon: modal editing

(use-package boon
  :ensure t
  :demand t
  :load-path "~/dev/boon"
  :diminish boon-local-mode
  :config
  (require 'boon-emacs)

  (bind-key "H" #'avy-goto-word-1 boon-command-map)
  (bind-key "v" #'scroll-up-command boon-command-map)
  (bind-key "V" #'scroll-down-command boon-command-map)
  (bind-key "/" #'occur boon-command-map)
  (bind-key "\\" #'indent-region boon-command-map)
  (bind-key "@" #'boon-switch-mark boon-command-map)

  (bind-key "i" #'counsel-imenu boon-goto-map)
  (bind-key "o" #'counsel-outline boon-goto-map)
  (bind-key "w" #'avy-goto-word-1 boon-goto-map)

  (setq boon-insert-cursor-type 'box)

  (boon-mode))

Avy

(use-package avy
  :ensure t
  :bind (("M-g g" . avy-goto-line)
         ("M-g M-g" . avy-goto-line)
         ("M-g M-s" . avy-goto-word-1)
         ("M-g M-r" . avy-copy-region)))

Ivy

(use-package ivy
  :ensure t
  :demand t
  :bind ("C-c C-r" . ivy-resume)
  :config
  (ivy-mode 1)
  (setq ivy-use-virtual-buffers t
        ivy-count-format "(%d/%d) "
        ivy-height 10
        ivy-re-builders-alist '((t . ivy--regex-ignore-order))
        magit-completing-read-function 'ivy-completing-read)
  :diminish ivy-mode)

Counsel

(use-package counsel
  :ensure t
  :demand t
  :bind (("C-x d" . counsel-dired))
  :config
  (counsel-mode 1)
  (setq counsel-grep-base-command
        "rg -i -M 120 --no-heading --line-number --color never '%s' %s"
        ivy-initial-inputs-alist '((counsel-minor . "^+")
                                   (counsel-package . "^+")
                                   (counsel-org-capture . "^")
                                   (counsel-M-x . "\\b")
                                   (counsel-describe-function . "\\b")
                                   (counsel-describe-variable . "\\b")
                                   (org-refile . "\\b")
                                   (org-agenda-refile . "\\b")
                                   (org-capture-refile . "\\b")
                                   (Man-completion-table . "")
                                   (woman . "^")))
  :diminish counsel-mode)

(dkellner/exwm-bind-keys '("s-x" . counsel-M-x))
(defun dkellner/counsel-ssh-term (&optional initial-input)
  "Run `ssh` for a hosts configured in ~/.ssh/config.
INITIAL-INPUT can be given as the initial minibuffer input."
  (interactive)
  (ivy-read "ssh " (dkellner/list-ssh-hosts)
            :initial-input initial-input
            :action #'dkellner/counsel-ssh-term-action
            :caller 'dkellner/counsel-ssh-term))

(defun dkellner/list-ssh-hosts ()
  "Return all hosts defined in `~/.ssh/config` as list."
  (with-temp-buffer
    (insert-file-contents (s-concat (getenv "HOME") "/.ssh/config"))
    (keep-lines "^Host [^*]")
    (-map (lambda (line)
            (s-chop-prefix "Host " line))
          (s-split "\n" (buffer-string) t))))

(defun dkellner/counsel-ssh-term-action (x)
  "Run `ssh X` in a new vterm buffer."
  (with-ivy-window
    (dkellner/vterm-command (format "ssh %s" x))))

AMX

(use-package amx
  :ensure t)

Company

"Company" stands for "complete anything" is the name of an advanced auto-completion framework for Emacs.

(use-package company
  :ensure t
  :bind (("C-." . company-complete))
  :config
  (setq company-dabbrev-downcase nil
        company-dabbrev-ignore-case nil
        company-idle-delay nil)
  (global-company-mode 1)
  :diminish)

yasnippet

(use-package yasnippet
  :ensure t
  :config
  (yas-global-mode)
  :diminish yas-minor-mode)

(use-package yasnippet-snippets
  :ensure t)

undo-tree

(use-package undo-tree
  :ensure t
  :config
  (global-undo-tree-mode)
  (setq undo-tree-visualizer-diff t)
  :diminish undo-tree-mode)

(Auto-)Filling

(setq-default fill-column 79)

Project management

Projectile

(use-package projectile
  :load-path "~/dev/projectile"
  :config
  (define-key projectile-mode-map (kbd "C-c p") 'projectile-command-map)
  (dkellner/exwm-bind-keys '("s-<return>" . projectile-run-vterm))

  (setq projectile-require-project-root nil)
  :diminish projectile-mode)

(use-package counsel-projectile
  :ensure t
  :config
  (setq counsel-projectile-switch-project-action
        #'counsel-projectile-switch-project-action-vc)
  (counsel-projectile-mode 1))

ibuffer-projectile

(use-package ibuffer-projectile
  :ensure t
  :config
  (add-hook 'ibuffer-hook
            (lambda ()
              (ibuffer-projectile-set-filter-groups)
              (unless (eq ibuffer-sorting-mode 'alphabetic)
                (ibuffer-do-sort-by-alphabetic)))))

direnv

(use-package direnv
  :ensure t
  :config
  (setq direnv-always-show-summary nil)
  (direnv-mode))

Terminal Emulation and shell

Even if we try to control most functions of our computing environment directly from Emacs, the command line as an input paradigm is a useful one. It's simple to understand, composable and widely supported.

Emacs actually offers a wide range of ways to interact with a shell, but I find libvterm to be the best solution so far. All others suffer from idiosyncrasies when it comes to running CLI programs (especially curses-based ones), but that is the reason I use a terminal emulator in the first place.

I don't spend much time in the shell, except for running certain applications. For that reason, I'm not getting too fancy here - I just use Bash, with virtually no custom configuration.

libvterm

(use-package vterm
  :ensure t
  :load-path "~/dev/emacs-libvterm"
  :config
  (setq vterm-kill-buffer-on-exit t
        vterm-max-scrollback 10000
        vterm-timer-delay 0.05)

  (defun dkellner/vterm-command (command)
    "Run COMMAND in a new vterm buffer named *vterm COMMAND*."
    (interactive (list (read-shell-command "Shell command: ")))
    (let ((vterm-shell command))
      (vterm (format "*vterm %s*" command)))))

Bash

Enable directory tracking and some basic configuration for searching the history.

function vterm_printf() {
    if [ -n "$TMUX" ]; then
        # tell tmux to pass the escape sequences through
        # (Source: http://permalink.gmane.org/gmane.comp.terminal-emulators.tmux.user/1324)
        printf "\ePtmux;\e\e]%s\007\e\\" "$1"
    elif [ "${TERM%%-*}" = "screen" ]; then
        # GNU screen (screen, screen-256color, screen-256color-bce)
        printf "\eP\e]%s\007\e\\" "$1"
    else
        printf "\e]%s\e\\" "$1"
    fi
}

if [[ "$INSIDE_EMACS" = 'vterm' ]]; then
    function clear() {
        vterm_printf "51;Evterm-clear-scrollback";
        tput clear;
    }
fi

vterm_prompt_end() {
    vterm_printf "51;A$(whoami)@$(hostname):$(pwd)"
}

PS1='\$ \[$(vterm_prompt_end)\]'
set show-all-if-ambiguous on

"\e[A": history-search-backward
"\e[B": history-search-forward

TODO Investigate bug with colored prompt

  • using PS1="\e[1m\e[32m\$\e[0m "

  • prompt sometimes containes the first characters of previous command, which cannot be deleted (as if they were part of the prompt)

  • Maybe there is code calculating the length of PS1 also counting the color codes?

  • Create an issue upstream as soon as I can reproduce it with a minimal config.

Shell commands

(setq async-shell-command-buffer 'new-buffer
      async-shell-command-display-buffer nil)

Org

Use org-plus-contrib

(use-package org
  :ensure org-plus-contrib)

Basic configuration

(setq org-directory "~/org/"
      org-agenda-files '("~/org/main.org" "~/org/tickler.org" "~/org/calendars/personal.org" "~/org/calendars/birthdays.org")
      org-refile-use-outline-path 'file
      org-outline-path-complete-in-steps nil
      org-refile-targets '((nil :maxlevel . 2)
                           ("~/org/inbox.org" :level . 0)
                           ("~/org/main.org" :maxlevel . 2)
                           ("~/org/calendars/personal.org" :level . 0)
                           ("~/org/pap.org" :maxlevel . 1)
                           ("~/org/calendars/puzzleandplay.org" :level . 0)
                           ("~/org/tickler.org" :maxlevel . 1)
                           ("~/org/bookmarks.org" :maxlevel . 1)
                           ("~/org/someday.org" :maxlevel . 2))
      org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "WAITING(w)" "|" "DONE(d)")))

;; This list contains tags I want to use in almost any file as they are tied to
;; actionable items (e.g. GTD contexts).
(setq org-tag-alist `((:startgroup)
                      ("@laptop" . ,(string-to-char "l"))
                      ("@phone" . ,(string-to-char "p"))
                      ("@home" . ,(string-to-char "h"))
                      ("@errands" . ,(string-to-char "e"))
                      (:endgroup)
                      ("@nhi" . ,(string-to-char "n"))
                      ("@work" . ,(string-to-char "w"))))

(setq org-startup-folded 'content
      org-log-into-drawer t
      org-agenda-todo-ignore-scheduled 'all
      org-agenda-todo-ignore-deadlines 'all
      org-agenda-tags-todo-honor-ignore-options t
      org-agenda-window-setup 'current-window
      org-agenda-restore-windows-after-quit nil
      org-time-clocksum-format "%d:%02d"
      org-enforce-todo-dependencies t
      org-columns-default-format "%40ITEM(Task) %3Priority(Pr.) %16Effort(Estimated Effort){:} %CLOCKSUM{:}"
      org-export-with-sub-superscripts nil
      org-export-allow-bind-keywords t
      org-default-priority ?C)

Capturing

Templates

(setq org-capture-templates
      '(("i" "Inbox" entry (file "~/org/inbox.org")
         "* %?\nCreated: %U")
        ("I" "Inbox (with link)" entry (file "~/org/inbox.org")
         "* %?\n%a\nCreated: %U")
        ("c" "Cookbook" entry (file "~/org/cookbook.org")
         "%(org-chef-get-recipe-from-url)"
         :empty-lines 1)))

(use-package ol-notmuch)

Use the same window

(use-package org-capture
  :config
  (defun dkellner/org-pop-to-buffer (&rest args)
    "Use `pop-to-buffer' instead of `switch-to-buffer' to open buffer.'"
    (let ((buf (car args)))
      (pop-to-buffer
       (cond ((stringp buf) (get-buffer-create buf))
             ((bufferp buf) buf)
             (t (error "Invalid buffer %s" buf))))))

  (advice-add #'org-switch-to-buffer-other-window
              :override #'dkellner/org-pop-to-buffer)

  (defun dkellner/org-capture-place-template (oldfun &rest args)
    "Don't delete other windows in `org-capture-place-template'."
    (cl-letf (((symbol-function #'delete-other-windows) #'ignore))
      (apply oldfun args)))

  (advice-add #'org-capture-place-template
              :around #'dkellner/org-capture-place-template))

Capture buffers should start in insert state

(use-package org
  :after boon
  :hook (org-capture-mode . boon-set-insert-like-state))

Agenda

Customizing the agenda view

(setq org-agenda-custom-commands
      '(("h" "Home"
         ((agenda "" ((org-agenda-span 'day)))
          (todo "TODO"
                ((org-agenda-sorting-strategy
                  '(priority-down tag-up))))))
        ("w" "Work"
         ((agenda "" ((org-agenda-span 'day)))
          (todo "TODO"
                ((org-agenda-sorting-strategy
                  '(priority-down tag-up)))))
         ((org-agenda-files
           (append org-agenda-files '("~/org/pap.org" "~/org/calendars/puzzleandplay.org")))
          (org-super-agenda-groups
           (append org-super-agenda-groups '((:name "@work" :tag "@work"))))))))

(use-package org-super-agenda
  :ensure t
  :config
  (setq org-super-agenda-groups
        '((:name "@laptop"
                 :tag "@laptop")
          (:name "@phone"
                 :tag "@phone")
          (:name "@home"
                 :tag "@home")
          (:name "@errands"
                 :tag "@errands")))
  (org-super-agenda-mode 1))

Habits

(require 'org-habit)

Keybindings

(bind-key "C-c a" #'org-agenda)
(bind-key "C-c c" #'org-capture)
(bind-key "C-c l" #'org-store-link)

Literate Programming

(setq org-src-tab-acts-natively t
      org-edit-src-content-indentation 0
      org-confirm-babel-evaluate nil)

(org-babel-do-load-languages
 'org-babel-load-languages
 '((emacs-lisp . t)
   (shell . t)
   (python . t)))

Expand snippets like "<s"

(require 'org-tempo)

Prettification

(setq org-ellipsis " ⤵")

(use-package org-bullets
  :ensure t
  :hook (org-mode . org-bullets-mode)
  :config
  (setq org-bullets-bullet-list '("◉" "❃" "✿" "✤")))

Use org-mode for *scratch*

(setq initial-major-mode 'org-mode
      initial-scratch-message nil)

Visual indentation instead of actual spaces

(use-package org-indent
  :hook (org-mode . org-indent-mode)
  :diminish)

org-store-link für qutebrowser

(defun dkellner/exwm-get-qutebrowser-url ()
  "Rather crude way of extracting the current URL in qutebrowser.

In qutebrowser, 'u' has to be bound to 'yank pretty-url'."
  (exwm-input--fake-key 'u)
  (sleep-for 0.05)
  (gui-backend-get-selection 'CLIPBOARD 'STRING))

(defun dkellner/org-store-link-qutebrowser ()
  "Store a link to the url of a qutebrowser buffer."
  (when (and (equal major-mode 'exwm-mode)
             (string= exwm-instance-name "qutebrowser"))
    (org-store-link-props
     :type "http"
     :link (dkellner/exwm-get-qutebrowser-url)
     :description exwm-title)))

(use-package org
  :config
  (org-link-set-parameters "http" :store #'dkellner/org-store-link-qutebrowser))

org-caldav

(use-package org-caldav
  :ensure t
  :config
  (setq org-caldav-url "https://nextcloud.noidea.info/remote.php/dav/calendars/dkellner"
        org-caldav-calendars '((:calendar-id "personal"
                                :files ("~/org/calendars/personal.org" "~/org/calendars/personal.org_archive")
                                :inbox "~/org/calendars/personal.org")
                               (:calendar-id "contact_birthdays"
                                :files ("~/org/calendars/birthdays.org")
                                :inbox "~/org/calendars/birthdays.org")
                               (:calendar-id "puzzle-play"
                                :files ("~/org/calendars/puzzleandplay.org" "~/org/calendars/puzzleandplay.org_archive")
                                :inbox "~/org/calendars/puzzleandplay.org"))))
(defun dkellner/archive-old-calendar-entries ()
  "Archive all entries older than 30 days in all calendar files.

Calendar files are all *.org files in `org-caldav-calendars',
this excludes *.org_archive files."
  (interactive)
    (dkellner/org-archive-all-older 30))

(defun dkellner/org-archive-all-older (days &optional tag)
  "Archive sublevels of the current tree with timestamps older than DAYS.
If the cursor is not on a headline, try all level 1 trees.  If
it is on a headline, try all direct children.
When TAG is non-nil, don't move trees, but mark them with the ARCHIVE tag.

See `org-archive-all-old'."
  (org-archive-all-matches
   (lambda (_beg end)
     (let (ts)
       (and (re-search-forward org-ts-regexp end t)
            (setq ts (match-string 0))
            (< (org-time-stamp-to-now ts) (- days))
            (if (not (looking-at
                      (concat "--\\(" org-ts-regexp "\\)")))
                (concat "old timestamp " ts)
              (setq ts (concat "old timestamp " ts (match-string 0)))
              (and (< (org-time-stamp-to-now (match-string 1)) (- days))
                   ts)))))
   tag))

org-chef

(use-package org-chef
  :ensure t)

Magit

(use-package magit
  :ensure t
  :config
  (setq magit-display-buffer-function
        #'magit-display-buffer-same-window-except-diff-v1)
  (magit-auto-revert-mode 1))

E-Mail

(defun dkellner/fetch-mail ()
  "Fetch mail."
  (interactive)
  (async-shell-command "~/hacks/fetch-and-index-mail.sh"))

(use-package notmuch
  :ensure t
  :config
  (setq mail-host-address (system-name)
        sendmail-program "msmtp"
        message-kill-buffer-on-exit t
        message-send-mail-function 'message-send-mail-with-sendmail
        message-sendmail-extra-arguments '("--read-envelope-from")
        message-sendmail-f-is-evil t
        notmuch-fcc-dirs '(("dominik.kellner@fotopuzzle.de"
                            . "puzzleandplay/.sent")
                           (".*" . "dkellner/.sent"))))

UI

Themes

Everybody's got one: their favorite theme. In my case I've always configured at least a dark and a light one, and I switch between them based on lighting conditions (e.g. when I'm working outside I'm likely to use the light theme).

This is another area where going "all-in" Emacs really shines: Switching your theme will conveniently affect all of your computing.

(setq custom--inhibit-theme-enable nil)

(use-package gruvbox-theme
  :ensure t
  :after boon
  :config
  (defun dkellner/load-dark-theme ()
    (interactive)
    (load-theme 'gruvbox-dark-hard t)
    (custom-theme-set-faces
     'gruvbox-dark-hard
     '(hl-line ((t (:background "#333333"))))
     '(ivy-posframe ((t (:background "#333333"))))
     '(mode-line ((t (:foreground "#ebdbb2" :background "#2b3c44"))))
     '(mode-line-inactive ((t (:foreground "#1d2021" :background "#1d2021"))))
     '(mode-line-buffer-id ((t (:foreground "#ffffc8" :weight bold))))
     '(internal-border ((t (:background "#303030"))))
     '(window-divider ((t (:foreground "#303030"))))
     '(window-divider-first-pixel ((t (:foreground "#303030"))))
     '(window-divider-last-pixel ((t (:foreground "#303030"))))
     '(org-block ((t (:background nil))))
     '(org-block-begin-line ((t (:foreground "#777777" :background nil))))
     '(org-block-end-line ((t (:foreground "#777777" :background nil)))))

    (setq boon-insert-cursor-color "#fb4933"
          boon-command-cursor-color "#b8bb26"
          boon-default-cursor-color "#83a598"))

  (defun dkellner/load-light-theme ()
    (interactive)
    (load-theme 'gruvbox-light-hard t)
    (custom-theme-set-faces
     'gruvbox-light-hard
     '(ivy-posframe ((t (:background "#e3e3e3"))))
     '(mode-line ((t (:background "#87afaf" :foreground "#ffffff"))))
     '(mode-line-inactive ((t (:foreground "#f9f5d7" :background "#f9f5d7"))))
     '(mode-line-buffer-id ((t (:foreground "#ffffc8" :weight bold))))
     '(internal-border ((t (:background "#d5c4a1"))))
     '(window-divider ((t (:foreground "#d5c4a1"))))
     '(window-divider-first-pixel ((t (:foreground "#d5c4a1"))))
     '(window-divider-last-pixel ((t (:foreground "#d5c4a1"))))
     '(org-block ((t (:background nil))))
     '(org-block-begin-line ((t (:foreground "#777777" :background nil))))
     '(org-block-end-line ((t (:foreground "#777777" :background nil)))))

    (setq boon-insert-cursor-color "#9d0006"
          boon-command-cursor-color "#79740e"
          boon-default-cursor-color "#076678"))

  (dkellner/load-dark-theme))

TODO Don't hardcode colors here, inherit from other faces

Font

(add-to-list 'default-frame-alist '(font . "Meslo LG M 12"))

Mode-line

(use-package all-the-icons
  :ensure t)

(column-number-mode 1)
(setq mode-line-position
      '((line-number-mode ("%l" (column-number-mode ":%c"))))
      eol-mnemonic-unix nil)
(setq-default mode-line-format
              '("%e"
                mode-line-front-space

                (:eval (when current-input-method-title
                         (format "%s " current-input-method-title)))

                mode-line-client

                (:eval
                 (let* ((props (-concat `(:height ,(/ all-the-icons-scale-factor 1.6)
                                                  :v-adjust 0)
                                        (cond
                                         (buffer-read-only '(:face (:foreground "gray85")))
                                         ((buffer-modified-p) '(:face (:foreground "red"))))))
                        (icon (apply #'all-the-icons-icon-for-mode
                                     (-concat (list major-mode) props))))
                   (if (not (eq icon major-mode)) icon
                     (apply #'all-the-icons-icon-for-mode 'text-mode props))))

                " "
                mode-line-buffer-identification
                " "
                mode-line-position
                " "
                mode-line-modes

                mode-line-misc-info
                mode-line-end-spaces))

Remove distractions

When you're using unclutter or similar to hide the mouse pointer, then setting mouse-highlight to nil is a must. Without, e.g. the agenda buffer will still keep highlighting the line the now invisible pointer is on.

(diminish 'auto-revert-mode)
(setq mouse-highlight nil
      ring-bell-function 'ignore)

window-divider

(use-package frame
  :config
   (setq window-divider-default-right-width 15
         window-divider-default-bottom-width 15
         window-divider-default-places t)
   (window-divider-mode))

Fringe

(use-package fringe
  :config
  (fringe-mode '(7 . 1)))

Transparency

(add-to-list 'default-frame-alist '(alpha . 90))

Browsing the web

Qutebrowser

(defun dkellner/browse-url-qutebrowser (url &optional new-window)
  "Ask qutebrowser to load URL."
  (interactive (browse-url-interactive-arg "URL: "))
  (let* ((url (browse-url-encode-url url))
         (process-environment (browse-url-process-environment)))
    (apply 'start-process
           (concat "qutebrowser " url)
           nil
           "qutebrowser"
           (list "--override-restore" "--target" "window" url))))

Set up a Hydra

(setq browse-url-browser-function #'dkellner/browse-url-qutebrowser)

(defun dkellner/browse-url-interactive-arg (prompt)
  (let ((url-at-point (lambda () (thing-at-point 'url t))))
    (advice-add 'browse-url-url-at-point :override url-at-point)
    (prog1
        (browse-url-interactive-arg prompt)
      (advice-remove 'browse-url-url-at-point url-at-point))))

(defun dkellner/browse-url (url-or-query &rest args)
  "Ask a WWW browser to load URL-OR-QUERY.

This behaves like `browse-url', with some differences:

1. It sets `default-directory' of the browser buffer to
\"~/\". This way the browser buffers will not be associated with
any projects by Projectile.

2. It overrides `browse-url-url-at-point' so that it only uses
real URLs as default, not prefixing any possible filename with
\"http://\".

3. If URL-OR-QUERY contains spaces, it is considered a search
query and opened with a search engine."
  (interactive (dkellner/browse-url-interactive-arg "URL: "))
  (let ((default-directory "~/")
        (url (if (cl-search " " url-or-query)
                 (format "https://duckduckgo.com/?q=%s" (url-encode-url url-or-query))
               url-or-query)))
    (apply #'browse-url url args)))

(defhydra dkellner/browse (:exit t)
  "Browse"
  ("o" #'dkellner/browse-url "url or query")
  ("b" #'dkellner/open-browser-bookmark "bookmark")
  ("we" (dkellner/search-online
         "https://www.wikipedia.org/search-redirect.php?language=en&go=Go&search=%s")
   "wikipedia")
  ("wd" (dkellner/search-online
         "https://www.wikipedia.org/search-redirect.php?language=de&go=Go&search=%s")
   "wikipedia"))

Bookmarks with org-mode

(require 'map)

(bind-key "C-c b" #'dkellner/open-browser-bookmark)

(defcustom dkellner-browser-bookmarks-file "~/org/bookmarks.org"
  "Org-file containing bookmarks as HTTP(S)-URLs.

Currently only a very strict structure is supported, i.e. the
first level headlines will be treated as sections/groups and the
second level ones as bookmarks.")

(defun dkellner/open-browser-bookmark ()
  "Interactively selects and opens a bookmark in the default browser.

It uses `org-open-link-from-string' and thus `browse-url'
internally for actually sending the URL to the browser. You
should refer to its documentation if you want to change the
browser."
  (interactive)
  (let ((bookmarks (dkellner/browser-bookmarks-in-org-file
                    dkellner-browser-bookmarks-file)))
    (ivy-read "Open bookmark: " (map-keys bookmarks)
              :require-match t
              :action (lambda (e) (org-open-link-from-string
                                   (cdr (assoc e bookmarks)))))))

(defun dkellner/browser-bookmarks-in-org-file (org-file)
  (with-current-buffer (find-file-noselect (expand-file-name org-file))
    (org-element-map (org-element-parse-buffer) 'headline
      (lambda (h)
        (when (= (org-element-property :level h) 2)
          (dkellner/browser-bookmark-to-key-value h))))))

(defun dkellner/browser-bookmark-to-key-value (bookmark)
  (let* ((section (org-element-property :parent bookmark))
         (section-prefix (concat (org-element-property :raw-value section)
                                 " :: "))
         (raw-value (org-element-property :raw-value bookmark))
         (regexp "\\[\\[\\(.+?\\)]\\[\\(.+?\\)]]"))
    (if (string-match regexp raw-value)
        `(,(concat section-prefix (match-string 2 raw-value)) .
          ,(match-string 1 raw-value))
      `(,(concat section-prefix raw-value) . ,raw-value))))

(defun dkellner/search-online (search-engine-url)
  (let ((query (url-encode-url (read-string "Query: "))))
  (dkellner/browse-url (format search-engine-url query))))

Programming

Clojure

(use-package cider
  :ensure t)

LSP

(use-package lsp-mode
  :ensure t
  :after boon
  :demand t
  :hook (((python-mode rustic-mode) . lsp))
  :config
  (setq lsp-enable-symbol-highlighting nil))

Keep LSP active when following xrefs outside the project

(defun xref-show-definitions-function (xrefs display-action)
  (let ((cw lsp--cur-workspace)
        (bw lsp--buffer-workspaces))
    (xref--show-xrefs xrefs display-action)
    (setq-local lsp--cur-workspace cw)
    (setq-local lsp--buffer-workspaces bw)
    (lsp-mode 1)))

Flycheck

(use-package flycheck
  :ensure t)

Language support

Docker

(use-package dockerfile-mode
  :ensure t)

Lisp

(use-package paredit
  :ensure t
  :hook ((emacs-lisp-mode . paredit-mode)
         (clojure-mode . paredit-mode))
  :config
  ;; I'm used to <C-left> and <C-right> for `left-word' and `right-word' so I
  ;; find it rather annoying that `paredit-mode' overwrites these with
  ;; `paredit-forward-barf-sexp' and `paredit-forward-slurp-sexp'.
  (define-key paredit-mode-map (kbd "<C-left>") nil)
  (define-key paredit-mode-map (kbd "<C-right>") nil)
  :diminish paredit-mode)

(use-package rainbow-delimiters
  :ensure t
  :hook ((emacs-lisp-mode . rainbow-delimiters-mode)
         (clojure-mode . rainbow-delimiters-mode)))

Emacs Lisp

(use-package eldoc
  :hook (emacs-lisp-mode . eldoc-mode))

(use-package macrostep
  :ensure t
  :bind (:map emacs-lisp-mode-map
              ("C-c e" . macrostep-expand)))

;; Make the use of sharp-quote more convenient.
;; See http://endlessparentheses.com/get-in-the-habit-of-using-sharp-quote.html
(defun endless/sharp ()
  "Insert #' unless in a string or comment."
  (interactive)
  (call-interactively #'self-insert-command)
  (let ((ppss (syntax-ppss)))
    (unless (or (elt ppss 3)
                (elt ppss 4)
                (eq (char-after) ?'))
      (insert "'"))))
(bind-key "#" #'endless/sharp emacs-lisp-mode-map)

Markdown

(use-package markdown-mode
  :ensure t)

Nix

(use-package nix-mode
  :ensure t
  :mode ("\\.nix\\'" . nix-mode))

PHP, HTML

(use-package web-mode
  :ensure t
  :config
  (add-to-list 'auto-mode-alist '("\\.php\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.html\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.phtml\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.tpl\\.php\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.[agj]sp\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.as[cp]x\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.erb\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.mustache\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.djhtml\\'" . web-mode))
  (setq-default web-mode-markup-indent-offset 2)
  (setq-default web-mode-css-indent-offset 2)
  (setq-default web-mode-code-indent-offset 2))

Rust

(use-package rustic
  :ensure t
  :config
  (setq rustic-format-on-save nil))

YAML

(use-package yaml-mode
  :ensure t)

(use-package highlight-indentation
  :ensure t
  :hook (yaml-mode . highlight-indentation-current-column-mode)
  :diminish highlight-indentation-current-column-mode)

Misc

Helpful

(use-package helpful
  :ensure t
  :config
  (setq counsel-describe-function-function #'helpful-callable
        counsel-describe-variable-function #'helpful-variable))

pdf-tools

(use-package pdf-tools
  :ensure t
  :config
  (require 'pdf-occur)
  (pdf-tools-install-noverify))

password-store

(use-package password-store
  :ensure t
  :config
  (dkellner/exwm-bind-keys
   '("s-p" . password-store-copy)
   '("s-P" . dkellner/password-store-copy-username)))

(defun dkellner/password-store-copy-username (entry)
  "Add username for ENTRY into the kill ring.

Clear previous username/password from the kill ring.  Pointer to
the kill ring is stored in `password-store-kill-ring-pointer'.
Username/password is cleared after
`password-store-time-before-clipboard-restore' seconds."
  (interactive (list (password-store--completing-read)))
  (password-store-get-field
   entry
   "username"
   (lambda (username)
     (password-store--save-field-in-kill-ring entry username "username"))))

diff-hl

(use-package diff-hl
  :ensure t
  :hook (((prog-mode conf-mode) . turn-on-diff-hl-mode)
         (magit-post-refresh . diff-hl-magit-post-refresh))
  :config
  (setq diff-hl-draw-borders t))

recentf

Auto-cleanup of recently used files is disabled, because it causes freezes when remote files are not accessible anymore.

(use-package recentf
  :demand t
  :config
  (setq recentf-max-saved-items 250
        recentf-auto-cleanup 'never)
  (add-to-list 'recentf-exclude no-littering-etc-directory)
  (add-to-list 'recentf-exclude no-littering-var-directory)
  (add-to-list 'recentf-exclude "^/\\(?:ssh\\|su\\|sudo\\)?:")
  (recentf-mode 1))

olivetti-mode

Olivetti is a nice little mode if you want to focus on writing one document.

(use-package olivetti
  :ensure t
  :custom
  (olivetti-body-width 90))

Dired

(use-package dired
  :bind (("C-x C-d" . counsel-dired))
  :config
  (require 'dired-x)
  (setq dired-listing-switches "-ahl"
        dired-omit-files "^\\.")
  (add-hook 'dired-mode-hook
            (lambda () (dired-omit-mode))))

Performance shenanigans

Startup

Inhibit implied frame resizing

(setq frame-inhibit-implied-resize t)

Always use left-to-right text

(setq-default bidi-paragraph-direction 'left-to-right)

GC-Tuning

(setq gc-cons-threshold (* 100 1024 1024))

Read bigger chunks from external processes

(setq read-process-output-max (* 1024 1024))

Playground

Often I get quite excited about all the great new packages out there and try them out immediately. Sometimes only to find myself forgetting about these new additions to my config and then they go unnoticed until I stumple upon them again months later.

This section is there to prevent it: I'm adding new packages, snippets etc. here for the purpose of reevaluating their usefulness after some time. If I don't use it as often as I thought I would, I just discard it again. Otherwise, I will move the entire section to a better place.

vlf

(use-package vlf
  :ensure t)

winner-mode

(use-package winner
  :config
  (winner-mode 1)
  (bind-key* "C-c <left>" #'dkellner/winner-undo/body))

(defhydra dkellner/winner-undo (:body-pre (winner-undo))
  ("<left>" winner-undo)
  ("<right>" winner-redo))

which-key

(use-package which-key
  :ensure t
  :diminish
  :config
  (which-key-mode))

hledger-mode

(use-package hledger-mode
  :ensure t
  :demand t
  :mode ("\\.journal\\'" "\\.hledger\\'")
  :hook (hledger-mode . (lambda () (setq-local tab-width 4)))
  :config
  (setq hledger-currency-string "EUR"))

Network manager

(use-package gnomenm
  :ensure t)

Customize startup

(setq inhibit-startup-screen t
      inhibit-startup-echo-area-message t
      inhibit-startup-message t)

Go to next char (like "t" in vi)

(use-package iy-go-to-char
  :ensure t
  :after boon
  :bind (:map boon-command-map ("h" . iy-go-up-to-char)))

Meta

Private configuration

(load "~/.emacs.d/private.el")

Remind about tangling configuration on exit

(defun dkellner/tangle-if-outdated (filename)
  "Ask to tangle FILENAME if it its corresponding `.el` file is older."
  (let ((el-file (concat (file-name-sans-extension filename) ".el")))
    (when (and (file-newer-than-file-p filename el-file)
               (y-or-n-p (format "%s is outdated. Tangle %s?" el-file filename)))
      (save-excursion
        (find-file filename)
        (org-babel-tangle))))
  t)

(defun dkellner/tangle-config ()
  "Ask to tangle init.org and private.org, if necessary."
  (dkellner/tangle-if-outdated "~/.emacs.d/init.org")
  (dkellner/tangle-if-outdated "~/.emacs.d/private.org"))

(add-hook 'kill-emacs-query-functions #'dkellner/tangle-config)