#+TITLE: Emacs Configuration
#+AUTHOR: Dominik Kellner
#+PROPERTY: header-args :tangle yes
* Initialization
** Lexical scope
#+begin_src emacs-lisp
;;; init.el --- user-init-file -*- lexical-binding: t -*-
;;;
;;; DO NOT EDIT THIS FILE!
;;; This file was automatically generated by tangling `init.org`.
#+end_src
* Package management
I use Nix with the [[https://github.com/nix-community/emacs-overlay][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
#+begin_src emacs-lisp
(require 'package)
(setq package-archives nil
package-enable-at-startup nil)
#+end_src
*** 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.
#+begin_src emacs-lisp
(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)))
#+end_src
* Common
These packages and functions are used by various other subsections.
** use-package
#+begin_src emacs-lisp
(require 'use-package)
#+end_src
** no-littering
=no-littering= needs to be loaded as early as possible, see
https://github.com/emacscollective/no-littering#usage for details.
#+begin_src emacs-lisp
(use-package no-littering
:ensure t)
#+end_src
** diminish
#+begin_src emacs-lisp
(use-package diminish
:ensure t)
#+end_src
** hydra
#+begin_src emacs-lisp
(use-package hydra
:ensure t)
#+end_src
** Defining global keybindings for EXWM
#+begin_src emacs-lisp
(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))))
#+end_src
* 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."
#+begin_src emacs-lisp
(use-package better-defaults
:ensure t
:config
(ido-mode -1)) ; I prefer ivy-mode
#+end_src
** Store customizations in a separate file
#+begin_src emacs-lisp
(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(when (file-exists-p custom-file)
(load custom-file))
#+end_src
** Remove trailing whitespace on save
#+begin_src emacs-lisp
(add-hook 'before-save-hook #'delete-trailing-whitespace)
#+end_src
** Kill the current buffer without confirmation
#+begin_src emacs-lisp
(bind-key "C-x k" #'dkellner/kill-current-buffer)
(defun dkellner/kill-current-buffer ()
"Kill the current buffer."
(interactive)
(kill-buffer (current-buffer)))
#+end_src
** Enable some commands that are disabled by default
#+begin_src emacs-lisp
(put 'upcase-region 'disabled nil)
(put 'downcase-region 'disabled nil)
(put 'narrow-to-region 'disabled nil)
#+end_src
** Unify the way Emacs is asking for confirmation
#+begin_src emacs-lisp
(fset 'yes-or-no-p 'y-or-n-p)
#+end_src
* EXWM
[[https://github.com/ch11ng/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 .xsession
#+begin_src sh :tangle ~/.xsession :shebang #!/bin/sh
source $HOME/.profile
autorandr -c
xset b off
xset s off
xset -dpms
setxkbmap de neo
light-locker &
nm-applet &
$HOME/hacks/git-autocommit.sh "$HOME/org/"
emacs --daemon -f exwm-enable
exec dbus-launch --exit-with-session emacsclient -c
#+end_src
** Configuration
#+begin_src emacs-lisp
(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 "dm-tool 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-output-plist '(6 "HDMI-1" 7 "HDMI-1"
8 "HDMI-1" 9 "HDMI-1"
0 "HDMI-1"))
(exwm-randr-enable)
(require 'exwm-systemtray)
(exwm-systemtray-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= "Navigator" exwm-instance-name)))
#+end_src
** Use M-y in EXWM buffers
#+begin_src emacs-lisp
(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)
#+end_src
** Window navigation
#+begin_src emacs-lisp
(dkellner/exwm-bind-keys
'("s-n" . windmove-left)
'("s-t" . windmove-right)
'("s-g" . windmove-up)
'("s-r" . windmove-down)
'("s-." . (lambda() (interactive)
(split-window-right) (windmove-right)))
'("s-," . (lambda() (interactive)
(split-window-below) (windmove-down)))
'("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))))
#+end_src
** Prevent suspending
Suspending Emacs causes EXWM to freeze. You can recover by sending =SIGUSR2= to
the running emacsclient process, but that is rather cumbersome.
#+begin_src emacs-lisp
(global-unset-key (kbd "C-z"))
(global-unset-key (kbd "C-x C-z"))
#+end_src
* 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=.
#+begin_src emacs-lisp
(dkellner/exwm-bind-keys
'("<XF86MonBrightnessUp>" . (lambda () (interactive)
(async-shell-command "~/hacks/backlightctl.sh inc")))
'("<XF86MonBrightnessDown>" . (lambda () (interactive)
(async-shell-command "~/hacks/backlightctl.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"))))
#+end_src
** Clipboard management
#+begin_src emacs-lisp
(use-package gpastel
:load-path "~/dev/gpastel"
:hook (exwm-init . gpastel-mode))
#+end_src
** 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.
#+begin_src emacs-lisp
(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)))
#+end_src
** Shutdown on critical battery level
#+begin_src emacs-lisp
(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 20.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))))
#+end_src
** 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=.
#+begin_src emacs-lisp
(defun dkellner/tor-browser ()
(interactive)
(async-shell-command "tor-browser"))
(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"))
#+end_src
*** TODO Write a macro
* Navigation and editing
** Boon: modal editing
#+begin_src emacs-lisp
(use-package boon
:ensure t
:load-path "~/dev/boon"
:demand t
:hook ((vterm-mode) . boon-set-insert-like-state)
:config
(require 'boon-emacs)
(bind-key "o" #'counsel-outline boon-goto-map)
(bind-key "w" #'avy-goto-word-1 boon-goto-map)
(boon-mode))
#+end_src
** Avy
#+begin_src emacs-lisp
(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)))
#+end_src
** Ivy
#+begin_src emacs-lisp
(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)
#+end_src
** Counsel
#+begin_src emacs-lisp
(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))
#+end_src
#+begin_src emacs-lisp
(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))))
#+end_src
** AMX
#+begin_src emacs-lisp
(use-package amx
:ensure t)
#+end_src
** Company
"Company" stands for "complete anything" is the name of an advanced
auto-completion framework for Emacs.
#+begin_src emacs-lisp
(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)
#+end_src
** yasnippet
#+begin_src emacs-lisp
(use-package yasnippet
:ensure t
:config
(yas-global-mode)
:diminish yas-minor-mode)
(use-package yasnippet-snippets
:ensure t)
#+end_src
** undo-tree
#+begin_src emacs-lisp
(use-package undo-tree
:ensure t
:config
(global-undo-tree-mode)
(setq undo-tree-visualizer-diff t)
:diminish undo-tree-mode)
#+end_src
** (Auto-)Filling
#+begin_src emacs-lisp
(setq-default fill-column 79)
#+end_src
* Project management
** Projectile
#+begin_src emacs-lisp
(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))
#+end_src
** ibuffer-projectile
#+begin_src emacs-lisp
(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)))))
#+end_src
** direnv
#+begin_src emacs-lisp
(use-package direnv
:ensure t
:config
(setq direnv-always-show-summary nil)
(direnv-mode))
#+end_src
* 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
#+begin_src emacs-lisp
(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)))))
#+end_src
** Bash
Enable [[https://github.com/akermu/emacs-libvterm#directory-tracking][directory tracking]] and some basic configuration for searching the
history.
#+begin_src conf :tangle ~/.bashrc
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)\]'
#+end_src
#+begin_src conf :tangle ~/.inputrc
set show-all-if-ambiguous on
"\e[A": history-search-backward
"\e[B": history-search-forward
#+end_src
*** 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
#+begin_src emacs-lisp
(setq async-shell-command-buffer 'new-buffer
async-shell-command-display-buffer nil)
#+end_src
* Org
** Use =org-plus-contrib=
#+begin_src emacs-lisp
(use-package org
:ensure org-plus-contrib)
#+end_src
** Basic configuration
#+begin_src emacs-lisp
(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/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)
#+end_src
** Capturing
*** Templates
#+begin_src emacs-lisp
(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)
#+end_src
*** Use =pop-to-buffer=
#+begin_src emacs-lisp
(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)
#+end_src
*** Capture buffers should start in insert state
#+begin_src emacs-lisp
(use-package org
:after boon
:hook (org-capture-mode . boon-set-insert-like-state))
#+end_src
** Agenda
*** Customizing the agenda view
#+begin_src emacs-lisp
(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))
#+end_src
** Habits
#+begin_src emacs-lisp
(require 'org-habit)
#+end_src
** Keybindings
#+begin_src emacs-lisp
(bind-key "C-c a" #'org-agenda)
(bind-key "C-c c" #'org-capture)
(bind-key "C-c l" #'org-store-link)
#+end_src
** Literate Programming
#+begin_src emacs-lisp
(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)))
#+end_src
** Expand snippets like "<s"
#+begin_src emacs-lisp
(require 'org-tempo)
#+end_src
** Prettification
#+begin_src emacs-lisp
(setq org-hide-emphasis-markers t
org-ellipsis " ⤵")
(use-package org-bullets
:ensure t
:hook (org-mode . org-bullets-mode)
:config
(setq org-bullets-bullet-list '("◉" "❃" "✿" "✤")))
#+end_src
** Use org-mode for =*scratch*=
#+begin_src emacs-lisp
(setq initial-major-mode 'org-mode
initial-scratch-message nil)
#+end_src
** Visual indentation instead of actual spaces
#+begin_src emacs-lisp
(use-package org-indent
:hook (org-mode . org-indent-mode)
:diminish)
#+end_src
** org-store-link for Firefox
See https://elmord.org/blog/?entry=20180214-exwm-org-capture .
#+begin_src emacs-lisp
(defun dkellner/exwm-get-firefox-url ()
"Rather crude way of extracting the current URL in Firefox."
(exwm-input--fake-key ?\C-l)
(sleep-for 0.05)
(exwm-input--fake-key ?\C-c)
(sleep-for 0.05)
(gui-backend-get-selection 'CLIPBOARD 'STRING))
(defun dkellner/org-firefox-store-link ()
"Store a link to the url of a Firefox buffer."
(when (and (equal major-mode 'exwm-mode)
(member exwm-class-name '("Firefox" "Firefox-esr")))
(org-store-link-props
:type "http"
:link (dkellner/exwm-get-firefox-url)
:description exwm-title)))
(use-package org
:config
(org-link-set-parameters "http" :store #'dkellner/org-firefox-store-link))
#+end_src
** org-caldav
#+begin_src emacs-lisp
(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 "emacs"
:files ("~/org/calendar.org")
:inbox "~/org/calendar.org")
(: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"))))
#+end_src
#+begin_src emacs-lisp
(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))
#+end_src
** org-chef
#+begin_src emacs-lisp
(use-package org-chef
:ensure t)
#+end_src
* Magit
#+begin_src emacs-lisp
(use-package magit
:ensure t
:config
(setq magit-display-buffer-function
#'magit-display-buffer-same-window-except-diff-v1)
(magit-auto-revert-mode 1))
#+end_src
* E-Mail
#+begin_src emacs-lisp
(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"))))
#+end_src
* 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.
#+begin_src emacs-lisp
(setq custom--inhibit-theme-enable nil)
(use-package gruvbox-theme
:ensure t
: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))))))
(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))))))
(dkellner/load-dark-theme))
#+end_src
*** TODO Don't hardcode colors here, inherit from other faces
** Font
#+begin_src emacs-lisp
(add-to-list 'default-frame-alist '(font . "Meslo LG M 13"))
#+end_src
** Mode-line
#+begin_src emacs-lisp
(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))
#+end_src
** 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.
#+begin_src emacs-lisp
(diminish 'auto-revert-mode)
(setq mouse-highlight nil
ring-bell-function 'ignore)
#+end_src
** Gaps between and around windows
#+begin_src emacs-lisp
(add-to-list 'default-frame-alist '(internal-border-width . 12))
(defun dkellner/exwm-set-workspace-border ()
(loop for frame in exwm-workspace--list do
(set-frame-parameter frame 'internal-border-width 12)))
(add-hook 'exwm-init-hook #'dkellner/exwm-set-workspace-border)
#+end_src
** window-divider
#+begin_src emacs-lisp
(use-package frame
:config
(setq window-divider-default-right-width 12
window-divider-default-bottom-width 12
window-divider-default-places t)
(window-divider-mode))
#+end_src
** Fringe
#+begin_src emacs-lisp
(use-package fringe
:config
(fringe-mode '(7 . 1)))
#+end_src
* Browsing the web
** Set up a Hydra
#+begin_src emacs-lisp
(setq browse-url-browser-function #'browse-url-firefox
browse-url-firefox-arguments '("-p" "default"))
(defun dkellner/browse-url (url &rest args)
"Ask a WWW browser to load URL.
This behaves like `browse-url', but sets `default-directory' of
the browser buffer to \"~/\". This way the browser buffers will
not be associated with any projects by Projectile."
(interactive (browse-url-interactive-arg "URL: "))
(let ((default-directory "~/"))
(apply #'browse-url url args)))
(defhydra dkellner/browse (:exit t)
"Browse"
("o" #'dkellner/browse-url "url")
("b" #'dkellner/open-browser-bookmark "bookmark")
("d" (dkellner/search-online "https://duckduckgo.com/?q=%s") "duckduckgo")
("w" (dkellner/search-online
"https://www.wikipedia.org/search-redirect.php?language=en&go=Go&search=%s")
"wikipedia"))
#+end_src
** Bookmarks with org-mode
#+begin_src emacs-lisp
(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 (read-string "Query: ")))
(dkellner/browse-url (format search-engine-url query))))
#+end_src
* Programming
** LSP
#+begin_src emacs-lisp
(use-package lsp-mode
:ensure t
:after boon
:demand t
:hook (((python-mode rust-mode) . lsp))
:init
(bind-key "l" lsp-command-map boon-command-map))
#+end_src
** Keep LSP active when following xrefs outside the project
#+begin_src emacs-lisp
(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)))
#+end_src
** Flycheck
#+begin_src emacs-lisp
(use-package flycheck
:ensure t)
#+end_src
* Language support
** Docker
#+begin_src emacs-lisp
(use-package dockerfile-mode
:ensure t)
#+end_src
** Emacs Lisp
#+begin_src emacs-lisp
(use-package eldoc
:hook (emacs-lisp-mode . eldoc-mode))
(use-package paredit
:ensure t
:config
(add-hook 'emacs-lisp-mode-hook 'paredit-mode)
;; 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 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)
(use-package rainbow-delimiters
:ensure t
:hook (emacs-lisp-mode . rainbow-delimiters-mode))
#+end_src
** Markdown
#+begin_src emacs-lisp
(use-package markdown-mode
:ensure t)
#+end_src
** Nix
#+begin_src emacs-lisp
(use-package nix-mode
:ensure t
:mode ("\\.nix\\'" . nix-mode))
#+end_src
** PHP, HTML
#+begin_src emacs-lisp
(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))
#+end_src
** Rust
#+begin_src emacs-lisp
(use-package rust-mode
:ensure t
:config
(setq lsp-rust-clippy-preference "on"))
#+end_src
** YAML
#+begin_src emacs-lisp
(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)
#+end_src
* Misc
** Helpful
#+begin_src emacs-lisp
(use-package helpful
:ensure t
:config
(setq counsel-describe-function-function #'helpful-callable
counsel-describe-variable-function #'helpful-variable))
#+end_src
** pdf-tools
#+begin_src emacs-lisp
(use-package pdf-tools
:ensure t
:config
(require 'pdf-occur)
(pdf-tools-install-noverify))
#+end_src
** password-store
#+begin_src emacs-lisp
(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"))))
#+end_src
** diff-hl
#+begin_src emacs-lisp
(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))
#+end_src
** recentf
Auto-cleanup of recently used files is disabled, because it causes freezes when
remote files are not accessible anymore.
#+begin_src emacs-lisp
(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))
#+end_src
** olivetti-mode
Olivetti is a nice little mode if you want to focus on writing one document.
#+begin_src emacs-lisp
(use-package olivetti
:ensure t
:custom
(olivetti-body-width 90))
#+end_src
** Dired
#+begin_src emacs-lisp
(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))))
#+end_src
* Performance shenanigans
** Startup
*** Inhibit implied frame resizing
#+begin_src emacs-lisp
(setq frame-inhibit-implied-resize t)
#+end_src
** Always use left-to-right text
#+begin_src emacs-lisp
(setq-default bidi-paragraph-direction 'left-to-right)
#+end_src
** GC-Tuning
#+begin_src emacs-lisp
(setq gc-cons-threshold (* 100 1024 1024))
#+end_src
** Read bigger chunks from external processes
#+begin_src emacs-lisp
(setq read-process-output-max (* 1024 1024))
#+end_src
* 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
#+begin_src emacs-lisp
(use-package vlf
:ensure t)
#+end_src
** winner-mode
#+begin_src emacs-lisp
(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))
#+end_src
** which-key
#+begin_src emacs-lisp
(use-package which-key
:ensure t
:diminish
:config
(which-key-mode))
#+end_src
** hledger-mode
#+begin_src emacs-lisp
(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"))
#+end_src
** Network manager
#+begin_src emacs-lisp
(use-package gnomenm
:ensure t)
#+end_src
** Customize startup
#+begin_src emacs-lisp
(setq inhibit-startup-screen t
inhibit-startup-echo-area-message t
inhibit-startup-message t)
#+end_src
** Go to next char (like "t" in vi)
#+begin_src emacs-lisp
(use-package iy-go-to-char
:ensure t
:after boon
:bind (:map boon-command-map ("h" . iy-go-up-to-char)))
#+end_src
* Meta
** Private configuration
#+begin_src emacs-lisp
(load "~/.emacs.d/private.el")
#+end_src
** Remind about tangling configuration on exit
#+begin_src emacs-lisp
(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)
#+end_src