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.

31 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

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

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

Common

These packages and functions are used by various other subsections.

Keep ~/.emacs.d/ clean

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)

(when (fboundp 'startup-redirect-eln-cache)
  (startup-redirect-eln-cache
   (convert-standard-filename
    (expand-file-name  "var/eln-cache/" user-emacs-directory))))

diminish

(use-package diminish
  :ensure t)

hydra

(use-package hydra
  :ensure t)

Run commands in an external terminal

(defun dkellner/term-command (cmd &rest args)
  (let* ((cmd-with-args (s-join " " (cons cmd args)))
         (name (format "alacritty: %s" cmd-with-args)))
  (apply #'start-process
         name
         (format "*%s*" name)
         "alacritty"
         "--title"
         cmd-with-args
         "--command"
         cmd
         args)))

Run functions in dedicated frames

We set the title to "emacs-floating", so we can distinguish them from regular emacs frames in our sway configuration.

(defun dkellner/run-in-dedicated-frame (fn)
  (let ((frame (make-frame '((title . "emacs-floating")))))
    (select-frame frame)
    (funcall fn)))

(defun dkellner/run-in-minibuffer-frame (fn)
  (let ((frame (make-frame '((minibuffer . only)
                             (title . "emacs-floating")))))
    (select-frame frame)
    (unwind-protect
        (funcall fn)
      (delete-frame frame))))

(defun dkellner/dedicated-frame-p ()
  (equal "emacs-floating" (frame-parameter nil 'title)))

Sensible defaults

A good starting point: `better-defaults`

From https://git.sr.ht/~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 vertico

Load custom.el

(when (file-exists-p custom-file)
  (load custom-file))

Revert buffers when files on disk change

(setq global-auto-revert-non-file-buffers t)
(global-auto-revert-mode 1)

Remove trailing whitespace on save

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

Kill the current buffer without confirmation

(bind-key "C-x k" #'kill-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)
(put 'narrow-to-page 'disabled nil)
(put 'narrow-to-defun 'disabled nil)

Unify the way Emacs is asking for confirmation

(setq use-short-answers t)

recentf

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

(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))

Prevent suspending

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

Don't save duplicates in kill-ring

(setq kill-do-not-save-duplicates t)

Navigation and editing

Boon: modal editing

(use-package boon
  :ensure t
  :demand t
  :diminish boon-local-mode
  :config
  (setq boon-insert-conditions
        '((eq major-mode 'message-mode)
          (eq major-mode 'eshell-mode)))

  ;; Core commands
  (define-key boon-command-map "x" 'boon-x-map)
  (define-key boon-command-map "c" 'boon-c-god)
  (define-key boon-command-map "g" '("goto" . boon-goto-map))

  ;; Switch to insert mode
  (define-key boon-command-map "n" '("insert" . boon-set-insert-like-state))
  (define-key boon-command-map "N" 'boon-substitute-region)
  (define-key boon-command-map "r" '("open" . boon-open-next-line-and-insert))
  (define-key boon-command-map "R" 'boon-open-line-and-insert)

  ;; Other commands
  (define-key boon-command-map "z" 'boon-repeat-command)
  (define-key boon-command-map "t" 'boon-replace-by-character)
  (define-key boon-command-map "T" 'boon-enclose)
  (define-key boon-command-map "\"" 'boon-quote-character)
  (define-key boon-command-map "/" 'occur)
  (define-key boon-command-map "d" 'boon-take-region)
  (define-key boon-command-map "D" 'boon-treasure-region)
  (define-key boon-command-map "y" '("yank" . boon-splice))
  (define-key boon-command-map "Y" 'yank-pop)
  (define-key boon-command-map "q" '("quit" . quit-window))
  (define-key boon-command-map "\\" 'indent-region)
  (define-key boon-command-map "^" 'join-line)
  (define-key boon-command-map "." 'dkellner/eglot-actions/body)

  ;; Neo2-like movement
  (define-key boon-moves-map "l" '("previous" . previous-line))
  (define-key boon-moves-map "L" 'boon-smarter-upward)
  (define-key boon-moves-map "a" '("next" . next-line))
  (define-key boon-moves-map "A" 'boon-smarter-downward)
  (define-key boon-moves-map "i" '("backward" . backward-char))
  (define-key boon-moves-map "I" 'boon-smarter-backward)
  (define-key boon-moves-map "e" '("forward" . forward-char))
  (define-key boon-moves-map "E" 'boon-smarter-forward)
  (define-key boon-moves-map "u" 'boon-beginning-of-line)
  (define-key boon-moves-map "o" 'boon-end-of-line)

  ;; Other movements
  (define-key boon-moves-map "s" 'boon-forward-search-map)
  (define-key boon-moves-map "S" 'boon-backward-search-map)
  (define-key boon-moves-map "(" 'boon-beginning-of-expression)
  (define-key boon-moves-map ")" 'boon-end-of-expression)
  (define-key boon-moves-map "{" 'backward-paragraph)
  (define-key boon-moves-map "}" 'forward-paragraph)
  (define-key boon-moves-map "<" 'beginning-of-buffer)
  (define-key boon-moves-map ">" 'end-of-buffer)
  (define-key boon-moves-map "@" 'boon-switch-mark)

  ;; Selections
  (define-key boon-select-map "w" 'boon-select-word)
  (define-key boon-select-map "h" 'boon-select-paragraph)
  (define-key boon-select-map "H" 'boon-select-document)
  (define-key boon-select-map "s" 'boon-select-wim)
  (define-key boon-select-map "S" 'boon-select-sentence)
  (define-key boon-select-map "r" 'boon-select-justline)
  (define-key boon-select-map "g" 'boon-select-block)
  (define-key boon-select-map "q" 'boon-select-outside-quotes)
  (define-key boon-select-map "x" 'boon-select-outside-pairs)
  (define-key boon-select-map "c" 'boon-select-inside-pairs)
  (define-key boon-select-map "C" 'boon-select-comment)
  (define-key boon-select-map "V" 'boon-select-blanks)
  (define-key boon-select-map "v" 'boon-select-with-spaces)
  (define-key boon-select-map "z" 'boon-select-content)
  (define-key boon-select-map "t" 'boon-select-borders)
  (define-key boon-select-map "T" 'boon-select-org-tree)
  (define-key boon-select-map "G" 'boon-select-org-table-cell)

  ;; Extensions of the goto map
  (define-key boon-goto-map "w" 'avy-goto-subword-1)

  (define-key boon-forward-search-map "m" 'dkellner/flymake-goto-next-error)
  (define-key boon-backward-search-map "m" 'dkellner/flymake-goto-prev-error)

  (boon-mode))

Avy

(use-package avy
  :ensure t
  :bind (("M-g g" . avy-goto-line)
         ("M-g M-g" . avy-goto-line)))

yasnippet

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

(use-package yasnippet-snippets
  :ensure t)

(Auto-)Filling

(setq-default fill-column 79)

Vertico and friends

(use-package vertico
  :ensure t
  :config
  (setq vertico-cycle t)
  (vertico-mode))

(use-package marginalia
  :ensure t
  :config
  (marginalia-mode))

(use-package orderless
  :ensure t
  :custom
  (completion-styles '(orderless basic))
  (completion-category-overrides '((file (styles basic partial-completion)))))

Consult

(use-package consult
  :ensure t
  :init
  (require 'consult-imenu)
  :bind (("C-x b" . consult-buffer)
         ("C-x p b" . consult-project-buffer)
         ("M-y" . consult-yank-pop)
         ("M-g i" . consult-imenu)
         ("M-g I" . consult-imenu-multi)
         ("M-g o" . consult-outline)
         ("M-g f" . consult-flymake)
         ("M-s g" . consult-ripgrep)))

Completion

(use-package emacs
  :bind ("C-." . completion-at-point))

(use-package cape
  :ensure t
  :config
  (add-to-list 'completion-at-point-functions #'cape-file)
  (add-to-list 'completion-at-point-functions #'cape-dabbrev))

(use-package corfu
  :ensure t
  :custom
  (corfu-cycle t)
  :config
  ;; Silence the pcomplete capf, no errors or messages!
  (advice-add 'pcomplete-completions-at-point :around #'cape-wrap-silent)

  ;; Ensure that pcomplete does not write to the buffer
  ;; and behaves as a pure `completion-at-point-function'.
  (advice-add 'pcomplete-completions-at-point :around #'cape-wrap-purify)

  (global-corfu-mode 1))

multiple-cursors

(use-package multiple-cursors
  :ensure t
  :bind (("C-<" . mc/mark-all-like-this)
         ("C->" . mc/mark-next-like-this)))

unfill

(use-package unfill
  :ensure t
  :bind ("M-q" . unfill-toggle))

Project management

project.el

(use-package project
  :config
  (setq project-switch-commands
        '((project-find-file "Find file")
          (project-find-regexp "Find regexp")
          (project-find-dir "Find directory")
          (project-eshell "Eshell")
          (magit-project-status "Magit" ?m))))

direnv integration

(use-package envrc
  :ensure t
  :diminish
  :config
  (envrc-global-mode 1))

Org

Use current version of org and org-contrib

(use-package org
  :ensure t)

(use-package org-contrib
  :ensure t)

Basic configuration

(setq org-directory "~/org/"
      org-agenda-files '("~/org/main.org" "~/org/tickler.org" "~/org/areas/")
      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/cookbook.org" . (:level . 0))
                           ("~/org/pap.org" . (:maxlevel . 1))
                           (org-agenda-files . (:maxlevel . 2))
                           ("~/org/calendars/personal.org" . (:level . 0))
                           ("~/org/calendars/puzzleandplay.org" . (:level . 0))
                           ("~/org/bookmarks.org" . (:maxlevel . 1)))
      org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "WAITING(w)" "|" "DONE(d)" "CANCELLED(c)")))

;; 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)))

(setq org-startup-folded 'content
      org-log-done 'time
      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-duration-format 'h:mm
      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
      org-insert-heading-respect-content t)

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")))

(use-package ol-notmuch
  :ensure t)

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)))
      (switch-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))

Delete dedicated frame after capture

See https://www.reddit.com/r/orgmode/comments/uycc8m/comment/ia422x6/ .

(defun dkellner/delete-frame-after-org-capture (&optional oldfun &rest args)
  (when (and (dkellner/dedicated-frame-p)
             (not (eq this-command #'org-capture-refile)))
    (delete-frame)))

(advice-add #'org-capture-finalize
            :after #'dkellner/delete-frame-after-org-capture)
(advice-add #'org-capture-refile
            :after #'dkellner/delete-frame-after-org-capture)

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-use-time-grid nil
      org-agenda-skip-scheduled-if-done t
      org-agenda-skip-deadline-if-done t
      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 " ⤵"
 org-agenda-block-separator 9472)

(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)

Save org-mode buffers after refiling

(use-package org
  :config
  (advice-add #'org-refile :after #'org-save-all-org-buffers))

org-ql

(use-package org-ql
  :ensure t)

(defun dkellner/list-tasks-done-today ()
  (interactive)
  (let* ((today-str (format-time-string "%Y-%m-%d"))
         (pattern (concat "- State \"DONE\" *from \"TODO\" *\\[" today-str)))
    (org-ql-search (org-agenda-files)
      `(or (closed :on today)
           (regexp ,pattern)))))

org-auto-tangle

(use-package org-auto-tangle
  :ensure t
  :hook (org-mode . org-auto-tangle-mode)
  :diminish)

Magit

(use-package magit
  :ensure t
  :config
  (setq magit-display-buffer-function
        #'magit-display-buffer-same-window-except-diff-v1
        magit-section-initial-visibility-alist '((stashes . hide)
                                                 (unpushed . show))))

E-Mail

(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@puzzleyou.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 color-theme-sanityinc-tomorrow
  :ensure t
  :config
  (defun dkellner/load-light-theme ()
    (interactive)
    (disable-theme 'sanityinc-tomorrow-night)
    (load-theme 'sanityinc-tomorrow-day t)
    (custom-theme-set-faces
     'sanityinc-tomorrow-night
     '(fringe ((t (:background unspecified))))
     '(org-block ((t (:background unspecified))))))

  (defun dkellner/load-dark-theme ()
    (interactive)
    (disable-theme 'sanityinc-tomorrow-day)
    (load-theme 'sanityinc-tomorrow-night t)
    (custom-theme-set-faces
     'sanityinc-tomorrow-night
     '(fringe ((t (:background unspecified))))
     '(org-block ((t (:background unspecified))))))

  (defun dkellner/toggle-theme ()
    (interactive)
    (if (-contains? custom-enabled-themes 'sanityinc-tomorrow-night)
        (dkellner/load-light-theme)
      (dkellner/load-dark-theme)))

  (dkellner/load-dark-theme))

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)

Fringe and internal borders

(add-to-list 'default-frame-alist '(internal-border-width . 7))

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

Scrolling

(setq auto-window-vscroll nil
      fast-but-imprecise-scrolling t
      scroll-conservatively 101
      scroll-margin 3
      scroll-preserve-screen-position t)

Customize startup

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

which-key

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

Programming

Common

(use-package eldoc
  :diminish
  :config
  (setq eldoc-echo-area-use-multiline-p nil))

Flymake

(use-package flymake-diagnostic-at-point
  :ensure t
  :after flymake
  :hook (flymake-mode . flymake-diagnostic-at-point-mode))

(defun dkellner/flymake-goto-next-error ()
  "Call `flymake-goto-next-error' non-interactively to suppress
printing the error."
  (interactive)
  (flymake-goto-next-error))

(defun dkellner/flymake-goto-prev-error ()
  "Call `flymake-goto-prev-error' non-interactively to suppress
printing the error."
  (interactive)
  (flymake-goto-prev-error))

LSP (eglot)

(use-package eglot
  :after yasnippet
  :config
  (add-to-list 'eglot-server-programs '(rust-ts-mode "rust-analyzer"))

  (defhydra dkellner/eglot-actions (:exit t)
    "Eglot"
    ("a" #'eglot-code-actions "code actions")
    ("f" #'eglot-format "format")
    ("r" #'eglot-rename "rename")))

(use-package consult-eglot
  :ensure t)

Language support

Docker

(use-package dockerfile-mode
  :ensure t)

Justfile

(use-package just-mode
  :ensure t)

Lisp

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

Emacs Lisp

(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)

Go

(use-package go-mode
  :ensure t)

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 rust-ts-mode
  :hook (rust-ts-mode . eglot-ensure))

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

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/exit?"
  ("s" #'dkellner/shutdown "shutdown")
  ("r" #'dkellner/reboot "reboot")
  ("x" #'dkellner/exit-sway "exit sway"))

(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/exit-sway ()
  "Kill emacs properly and exit sway."
  (interactive)
  (dkellner/prepare-kill-and-run "swaymsg exit"))

(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)))

Helpful

(use-package helpful
  :ensure t
  :config
  (global-set-key (kbd "C-h f") #'helpful-callable)
  (global-set-key (kbd "C-h F") #'helpful-function)
  (global-set-key (kbd "C-h v") #'helpful-variable)
  (global-set-key (kbd "C-h k") #'helpful-key)
  (global-set-key (kbd "C-h C") #'helpful-command))

pdf-tools

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

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))

Dired

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

consult-ssh

(defun dkellner/open-ssh-term ()
  "Run `ssh` for a hosts configured in ~/.ssh/config.
INITIAL-INPUT can be given as the initial minibuffer input."
  (interactive)
  (let ((host (completing-read "ssh " (pcmpl-ssh-hosts))))
    (dkellner/term-command "ssh" host)))

Make shebang (#!) file executable when saved

(add-hook 'after-save-hook #'executable-make-buffer-file-executable-if-script-p)

Better completions for Eshell

(use-package pcmpl-args
  :ensure t)

Performance shenanigans

Startup

Inhibit implied frame resizing

(setq frame-inhibit-implied-resize t)

Better support for files with long lines

(setq-default bidi-paragraph-direction 'left-to-right
              bidi-inhibit-bpa t)
(global-so-long-mode 1)

GC-Tuning

(setq gc-cons-threshold (* 16 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)

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"))

org-roam

(use-package org-roam
  :ensure t
  :hook (after-init . org-roam-setup)
  :diminish
  :init (setq org-roam-v2-ack t)
  :config
  (setq org-roam-directory "~/org/roam"
        emacsql-sqlite3-executable (executable-find "sqlite3")
        org-roam-capture-templates
        '(("d" "default" plain "%?" :if-new
           (file+head "%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n")
           :unnarrowed t
           :immediate-finish t)))

  (defhydra dkellner/org-roam (:exit t)
    "org-roam"
    ("f" #'org-roam-node-find "find")
    ("i" #'org-roam-node-insert "insert")
    ("b" #'org-roam-buffer-toggle "backlinks"))

  (bind-key* "C-c r" #'dkellner/org-roam/body))

org-tree-slide

(use-package org-tree-slide
  :ensure t)

Meta

Private configuration

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