Elektrubadur

Emacs Configuration

Introduction

This is my Emacs configuration. It is available online in these forms:

Inspirations for this Emacs configuration include:

Some interesting properties of this configuration:

  • Almost all of the configuration is kept in an Org file.
  • The startup time is quite fast. There are some tweaks in init.el and early-init.el that makes helps with this, together with prudent use of autoloading, and in general trying to keep the number of add-on packages low.
  • Add-on packages are managed manually as Git submodules.

The configuration is organized so that basic configuration of Emacs comes first, then built-in packages that are bundles with Emacs, followed by the add-on packages that are kept in ~/.emacs.d/site-lisp/.

Here is my home page on EmacsWiki.

Set personal information

(customize-set-variable 'user-full-name "Björn Lindström")
(customize-set-variable 'user-mail-address "bkhl@elektrubadur.se")

Built-in functionality

Configuration of built-in Emacs behaviour and packages bundled with Emacs.

Start-up

use-package

The use-package macro makes it easier to configure add-on packages without generally having to remember how to set up autoloading &c.

(require 'use-package)

This section contains extensions to this macro to enable using packages from Git submodules without involving a package manager.

Provide default :load-path pointing to location of submodules

This makes the :load-package keyword in use-package calls default to a directory under /.emacs.d/site-lisp/ named the same as the package to be imported, if such a directory exists. For packages where for some reason the package name doesn't match the directory name, or that has its Lisp files in a subdirectory, an explicit :load-path needs to be added.

(setf (alist-get :load-path use-package-defaults)
      '((lambda (name args)
          (let ((path (expand-file-name (concat user-emacs-directory
                                                "site-lisp/"
                                                (symbol-name name)))))
            (when (file-exists-p path)
              (list path))))
        t))
Add :make-autoloads keyword extension to generate autoload files

Some packages rely on package.el (or equivalent) to generate and register an autoloads file. This custom keyword will do that for packages included without a package manager.

This is the function that actually generates the autoload file (if needed) and loads it.

Since make-directory-autoloads has no logic to work out if the file needs to be regenerated, this function will skip calling it the file exists already, and is newer than all the other .el files in the directory.

If a package has additional files in subdirectories and such, this function would not handle it, but then neither would package-generate-autoloads, as far as I can tell, so this should work for any packages supported by package.el.

(defun my/use-package-autoload-package (name package-directory)
  "Set up autoloading for package NAME in directory PACKAGE-DIRECTORY."
  (let* ((name (symbol-name name))
         (auto-file (expand-file-name (format "%s/%s-autoloads.el"
                                              package-directory
                                              name))))
    (when (or (not (file-exists-p auto-file))
              (let* ((autoloads-attributes
                      (file-attributes auto-file))
                     (autoloads-age
                      (file-attribute-modification-time
                       autoloads-attributes))
                     (autoloads-inode-number
                      (file-attribute-inode-number autoloads-attributes)))
                (seq-find (lambda (attributes)
                            (time-less-p autoloads-age
                                         (file-attribute-modification-time
                                          attributes)))
                          (mapcar #'cdr
                                  (directory-files-and-attributes
                                   package-directory
                                   nil
                                   (rx ".el" eos))))))
      (make-directory-autoloads package-directory auto-file))
    (load auto-file package-directory)))

Register the custom keyword. This is added first in the list, so that it will have access to the :load-path parameter, and so that it will load before :defer or other keywords that might cause this to run after the package is loaded.

(add-to-list 'use-package-keywords :make-autoloads)

This makes the keyword take boolean parameters similar to other keywords like :defer.

(defalias 'use-package-normalize/:make-autoloads
  'use-package-normalize-predicate)

The handler function is what injects the call to the function to generate the autoloads file when the use-package macro is expanded.

(defun use-package-handler/:make-autoloads (name _keyword arg rest state)
  (use-package-concat
     (mapcar #'(lambda (path)
                 `(my/use-package-autoload-package ',name ,path))
             (plist-get rest :load-path))
     (use-package-process-keywords name rest state)))

Keep customizations in separate file

This makes the Emacs customization interface store values in a separate file, instead of in init.el.

(customize-set-variable 'custom-file (concat user-emacs-directory "custom.el"))
(load custom-file :noerror)

Show init time on startup

(advice-add 'display-startup-echo-area-message
            :after
            (defun my/display-startup-echo-area-message ()
              (message "Emacs init time: %s" (emacs-init-time))))

Disable garbage collection when in minibuffer

Disable GC while minibuffer is open, and enabled again when it is closed. This helps prevent hanging while working in the minibuffer.

(add-hook 'minibuffer-setup-hook
          (defun my/disable-gc ()
            (setq gc-cons-threshold most-positive-fixnum)))
(add-hook 'minibuffer-exit-hook
          (defun my/default-gc ()
            (setq gc-cons-threshold my/default-gc-cons-threshold)))

Interface

Show line and column number in mode line

(line-number-mode)
(column-number-mode)

Emoji font

Enable Emoji font if available. ☃

This is run as a hook after the first graphical frame is created, as this will otherwise not work when Emacs is started in daemon mode, or by emacsclient.

(defun my/set-fontset-fonts (frame)
  (when (display-graphic-p frame)
    (dolist (font-spec '((#x2600 . #x26ff)
                         emoji))
      (set-fontset-font t font-spec
                        "Noto Color Emoji"))
    (remove-hook 'after-make-frame-functions
                 'my/set-fontset-fonts)))

(add-hook 'after-make-frame-functions #'my/set-fontset-fonts)

(my/set-fontset-fonts (selected-frame))

Disable bell

Disable warning bell, both the default audio one and the visual one.

(customize-set-variable 'ring-bell-function 'ignore)

Set window title

Set window title including current buffer or filename, along with system name. Use a straight or squiggly line to show if the buffer has modifications.

(setq frame-title-format
      '(
        "%b"
        (:eval (if (buffer-modified-p) " ⁓ " " — "))
        (:eval (system-name))))

Enable restoring exact window size

Setting this variable allows resizing window by pixels, rather than rounding to an exact number of lines or columns. This is needed to be able to restore back from fullscreen to original frame size in Gnome.

(customize-set-variable 'frame-resize-pixelwise t)

Disable message on new emacsclient frames

(customize-set-variable 'server-client-instructions nil)

Prompt before closing Emacs

(customize-set-variable 'confirm-kill-emacs 'y-or-n-p)

Set preferred dateformat

(calendar-set-date-style 'iso)

Allow undo of window layout changes

(winner-mode)

Preserve M-x command history between sessions

(savehist-mode)

Use saved point position in previously opened files

(save-place-mode)

Scrolling behaviour when moving cursor

When the cursor moves close to the edge of the screen, scroll only one line at time, but try to keep 5 rows within view.

(customize-set-variable 'scroll-conservatively 101)
(customize-set-variable 'scroll-margin 5)

Smooth scrolling with scroll wheel

(pixel-scroll-precision-mode)

Highlight error messages

In next-error buffers, highligt the currently visited error.

(customize-set-variable 'next-error-message-highlight t)

Make yes/no prompts shorter

(customize-set-variable 'use-short-answers t)

Don't show bookmarks in fringe

(customize-set-variable 'bookmark-set-fringe-mark nil)

Use bar cursor

(customize-set-variable 'cursor-type 'bar)

Show matching parenthesis context when offscreen

(customize-set-variable 'show-paren-context-when-offscreen 'overlay)

Switch windows with M-o

Bind M-o (by default bound to a rarely used command) to other-window.

(global-set-key (kbd "M-o") #'other-window)

Switch between windows with S-<direction>

(windmove-default-keybindings)

Key bindings

Disable C-z

Disabling C-z, which normally minimizes the window, which is rather distracting.

(keymap-global-unset "C-z")

Enable repeat maps for commands that have them

This adds ability to repat some common commands by repeating the last key in its binding.

(repeat-mode)

Mouse behaviour

Make middle-clicking mouse yank at point

(customize-set-variable 'mouse-yank-at-point t)

Save to kill ring when adjusting region with mouse

Setting this to non-empty means this won't happen for empty strings, like when accidentally dragging for less than a character's width.

(customize-set-variable 'mouse-drag-copy-region 'non-empty)

Documentation and help

Make apropos search more extensively

(customize-set-variable 'apropos-do-all t)

Use variable pitch in Info reader

(add-hook 'Info-mode-hook 'variable-pitch-mode)

Autoload if documentation is missing from autoload objects

(customize-set-variable 'help-enable-symbol-autoload t)

Show outlines in bindings description

(customize-set-variable 'describe-bindings-outline t)

Reuse help window if already shown

(customize-set-variable 'help-window-keep-selected t)

Buffers

Start with an empty scratch buffer.

(customize-set-variable 'inhibit-startup-screen t)
(customize-set-variable 'initial-scratch-message nil)

Use directory name in buffer names for files with same name

(customize-set-variable 'uniquify-buffer-name-style 'forward)

Allow remembering risky local variables

This overrides the Emacs settings that enforces having to accept local variables matching certain patterns every time they are used.

(advice-add 'risky-local-variable-p :override #'ignore)

Load .dir-locals.el files on remote hosts

(customize-set-variable 'enable-remote-dir-locals t)

Key bindings to kill/bury current buffer

Change the default keybinding for killing a buffer, C-x k, so that it kills the current buffer rather than prompting for a buffer. Instead C-x K is used for the previous default.

Also binds C-x M-k to bury the current buffer, a command that's not bound to any key by default.

(defun my/kill-this-buffer ()
  "Kill current buffer, prompting if there are unsaved changes."
  (interactive)
  (kill-buffer (current-buffer)))

(global-set-key (kbd "C-x k") #'my/kill-this-buffer)
(global-set-key (kbd "C-x K") #'kill-buffer)
(global-set-key (kbd "C-x M-k") #'bury-buffer)

Files

Start opening files from home directory

Unless overridden by a buffer, when prompting to open a file, start in the home directory.

(setq default-directory "~/")

Backup by copying

The default method here can break hardlinks.

(customize-set-variable 'backup-by-copying t)

Store backups in tmp directory

Store backups and autosaves in temporary-file-directory. This risks losing some data on a system crash, but I am not very concerned about that as generally my important files are in some kind of version control.

(customize-set-variable 'backup-directory-alist
      `((".*" . ,temporary-file-directory)))
(customize-set-variable 'auto-save-file-name-transforms
      `((".*" ,temporary-file-directory t)))

Offer to create parent directories on save

When saving a file to a directory that doesn't exist, offer to create it.

(add-hook
 'before-save-hook
 (defun my/ask-create-directory ()
   (when buffer-file-name
     (let ((dir (file-name-directory buffer-file-name)))
       (when
           (and
            (not (file-exists-p dir))
            (y-or-n-p
             (format
              "Directory %s does not exist. Create it?"
              dir)))
         (make-directory dir t))))))

Disable message when saving files

(customize-set-variable 'save-silently t)

Automatically sync updated files

If a file changes, automatically refresh buffers containing the file, so that it doesn't get out of sync.

(global-auto-revert-mode t)

Disable Emacs lock files

Disable use of those lock files with a .# prefix that Emacs by default creates. Since my ways of using Emacs rarely involves multiple Emacs instances opening the same file, they cause me more problems than they solve.

(customize-set-variable 'create-lockfiles nil)

dired

Make file sizes shown in dired human readable.

(customize-set-variable 'dired-listing-switches
      "-l --all --human-readable --group-directories-first")

tramp remote editing

Allow Tramp to write backups of root-owned files in /tmp, and ensure that Tramp uses path of remote shell on remote hosts.

(use-package tramp
  :custom
  (tramp-allow-unsafe-temporary-files t)
  :config
  (add-to-list 'tramp-remote-path 'tramp-own-remote-path))

Text editing

Bind Home/End to move to start/end of line

(global-set-key (kbd "<home>") #'move-beginning-of-line)
(global-set-key (kbd "<end>") #'move-end-of-line)

Change behaviour of M-z for zapping to character

Make M-z kill characters up to the character before the next occurrence of the selected character, instead of including it, which is generally more useful.

(global-set-key (kbd "M-z") #'zap-up-to-char)

Bind cycle-spacing to M-S-SPC

This edits whitespace around point by cycling between leaving only one space, deleting the space, and going back to what was there before.

(global-set-key (kbd "M-S-SPC") #'cycle-spacing)

Bind duplicate-dwim to M-R

Duplicates current line or active region.

(global-set-key (kbd "M-R") #'duplicate-dwim)

Use single space to delimit sentences

(customize-set-variable 'sentence-end-double-space nil)

Highlight selected region and apply changes to it

Highlight the region when the mark is active.

(transient-mark-mode t)

Set it so that if a selection is active, typed text will replace the selection.

(delete-selection-mode t)

Disable indentation using tabs.

(customize-set-variable 'indent-tabs-mode nil)

Set default line length to 80

(customize-set-variable 'fill-column 80)

Show character name in character description

When using C-x = to look up the character under the point, also show Unicode character name.

(customize-set-variable 'what-cursor-show-names t)

Automatically pair matching characters like parenthesis

Enable electric-pair-mode, which enables automatic insert of matching characters for example for parentheses.

(electric-pair-mode)

Save existing clipboard text into kill ring before replacing it

Prevents killing text in Emacs from irrevocably deleting things from the system clipboard.

(customize-set-variable 'save-interprogram-paste-before-kill t)

Enable downcase-region and upcase-region

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

Make replacements and completions preserve case

This also affects dabbrev completions.

(customize-set-variable 'case-replace nil)

Completion

dabbrev dynamic abbreviations

Swap dabbrev-completion and dabbrev-expand, which works nicer with Corfu.

Also make it ignore some buffers where it does not make sense.

(use-package dabbrev
  :bind (("M-/" . dabbrev-completion)
         ("C-M-/" . dabbrev-expand))
  :config
  (add-to-list 'dabbrev-ignored-buffer-regexps "\\` ")
  (add-to-list 'dabbrev-ignored-buffer-modes 'doc-view-mode)
  (add-to-list 'dabbrev-ignored-buffer-modes 'pdf-view-mode)
  (add-to-list 'dabbrev-ignored-buffer-modes 'tags-table-mode))

Programming

Render some keywords and operators as symbols

I use this to make lambda get rendered as λ in Emacs Lisp, and similar replacements in other languages.

(global-prettify-symbols-mode)

Some reusable character compositions.

Most of these are double-wide characters in the font that I use, meaning that for example will for me occupy the space of two normal characters.

In principle I should then be able to replace two characters with that one and everything should line up, but I've sometimes seen subtle alignment issues when doing that, so now use this method where you first draw the correct number of spaces, and then the character on top of them.

This also works when the replacement character is not actually the same width as the characters I'm replacing, like with and in my case.

This also incidentally means this should work regardless of the width of the glyph in your font.

(setq my/prettify-right-arrow
      '(?\s (Br . Bl) ?\s (Bc . Bc) ?→)
      my/prettify-double-right-arrow
      '(?\s (Br . Bl) ?\s (Bc . Bc) ?⇒)
      my/prettify-left-arrow
      '(?\s (Br . Bl) ?\s (Bc . Bc) ?←)
      my/prettify-double-colon
      '(?\s (Br . Bl) ?\s (Bc . Bc) ?∷)
      my/prettify-ellipsis
      '(?\s (Br . Bl) ?\s (Br . Bl) ?\s (Bc . Bc) ?…))

Simplified predicate to determine if a substitution should be applied, which makes them apply everywhere except for in strings. prettify-symbols-compose-predicate can be set to this to apply substitutions more liberally.

(defun my/prettify-symbols-compose-p (_start _end _match)
    (not (nth 3 (syntax-ppss))))

In programming modes, treat words in camel case symbols as separate.

(add-hook 'prog-mode-hook 'subword-mode)

Bind key to trigger compilation/recompilation

(define-key prog-mode-map (kbd "C-c b") #'compile)
(define-key prog-mode-map (kbd "C-c r") #'recompile)

flymake

Package for showing diagnostics from linters and similar interactively.

(autoload #'flymake-goto-next-error "flymake" nil t)
(autoload #'flymake-goto-prev-error "flymake" nil t)

(eval-after-load 'flymake
  '(progn
     (define-key flymake-mode-map (kbd "M-n") 'flymake-goto-next-error)
     (define-key flymake-mode-map (kbd "M-p") 'flymake-goto-prev-error)))

Eglot for language server protocol support

(use-package eglot
  :config
  (bind-key "C-c l f" 'eglot-format eglot-mode-map)
  (bind-key "C-c l r" 'eglot-rename eglot-mode-map))

This enables the Eglot LSP client.

See C for an example of how to configure it for a project.

Languages

C
(use-package cc-mode
  :custom
  (c-default-style '((java-mode . "java")
                     (awk-mode . "awk")
                     (other . "my")))
  :hook
  (c-mode . my/config-c-mode)
  :config
  (c-add-style "my"
               '((c-basic-offset . 2)
                 (c-comment-only-line-offset . 0)
                 (c-hanging-braces-alist . ((brace-list-open)
                                            (brace-entry-open)
                                            (substatement-open after)
                                            (block-close . c-snug-do-while)
                                            (arglist-cont-nonempty)))
                 (c-cleanup-list . (brace-else-brace))
                 (c-offsets-alist . ((statement-block-intro . +)
                                     (knr-argdecl-intro . 0)
                                     (substatement-open . 0)
                                     (substatement-label . 0)
                                     (label . 0)
                                     (statement-cont . +)))))
  (defun my/config-c-mode ()
    (setq-local prettify-symbols-alist
                `(("->" . ,my/prettify-right-arrow)))))

To use a language server through Eglot for C, you can create a .dir-locals.el file to run it in a container for C mode buffers, with the project directory mounted to the same path within the container to ensure paths sent to the LSP server matches those on the host system, and enable Eglot automatically when opening C mode buffers.

((c-mode
  . ((eval
      . (let ((root (project-root (project-current))))
          (setq-local eglot-server-programs
                      `((c-mode
                         "podman" "run" "--rm" "--interactive"
                         ,(concat "--volume=" root ":" root ":z")
                         ,(concat "--workdir=" root)
                         "ghcr.io/bkhl/lsp-containers/ccls:latest")))
          (eglot-ensure))))))
Containerfile/Dockerfile
(use-package dockerfile-ts-mode
  :mode (rx (or "/" bos)
            (or "Containerfile" "Dockerfile")
            (opt "." (*  (not (any "/"))))
            eos))
Go
(use-package go-ts-mode
  :init
  (defalias 'go-mode 'go-ts-mode)
  (defalias 'go-mod-mode 'go-mod-ts-mode)
  :mode
  ((rx ".go" eos) . go-mode)
  ((rx (or "/" bos) "go.mod" eos) . go-mod-mode)
  :custom (go-ts-mode-indent-offset 4)
  :config (defun my/config-go-ts-mode ()
            (setq-local tab-width 4)
            (setq-local prettify-symbols-alist
                        `(("<-" . ,my/prettify-left-arrow)
                          ("..." . ,my/prettify-ellipsis))))
  :hook (go-ts-mode . my/config-go-ts-mode))

Example .dir-locals.el to use the gopls language server with Eglot, also using it for code formatting on save.

((go-ts-mode
  . ((eval
      . (let ((root (project-root (project-current))))
          (setq-local eglot-server-programs
                      `((go-ts-mode
                         "podman" "run" "--rm" "--interactive"
                         ,(concat "--volume=" root ":" root ":z")
                         ,(concat "--workdir=" root)
                         "docker.io/lspcontainers/gopls:latest")))
          (add-hook 'before-save-hook #'eglot-format-buffer nil t)
          (eglot-ensure))))))
Perl
(use-package cperl-mode
  :custom
  (cperl-file-style "PBP")
  :init
  (add-to-list 'major-mode-remap-alist '(perl-mode . cperl-mode))
  :config
  (defun my/config-cperl-mode ()
    (setq-local prettify-symbols-compose-predicate
                #'my/prettify-symbols-compose-p
                prettify-symbols-alist
                `(("->" . ,my/prettify-right-arrow)
                  ("=>" . ,my/prettify-double-right-arrow)
                  ("::" . ,my/prettify-double-colon))))
  :hook
  (cperl-mode . my/config-cperl-mode))
Prolog
(use-package prolog
  :hook
  (prolog-mode . my/config-prolog-mode)
  :config
  (defun my/config-prolog-mode ()
    (setq-local prettify-symbols-alist
                `((":-" ,my/prettify-left-arrow)
                  ("->" ,my/prettify-right-arrow)))))

Version control

vc-diff

Make vc-diff imitate the diff format of Magit.

(customize-set-variable 'diff-font-lock-prettify t)

ediff

Make ediff use existing frame instead of creating new one

(customize-set-variable 'ediff-window-setup-function
                        'ediff-setup-windows-plain)

Project management

Detect Exercism exercises as projects.

This will make e.g. project-compile run commands with the appropriate working directory for Exercism excercises.

(add-hook 'project-find-functions
          (defun my/project-try-exercism (path)
            (when-let ((root (locate-dominating-file path ".exercism")))
              (cons 'transient (expand-file-name root)))))

Bug reference mode

Enable bug reference mode, and in Org mode override the keybinding to open links in the bug reference overlays.

(use-package bug-reference
  :custom
  (bug-reference-bug-regexp nil)
  (bug-reference-url-format nil)
  :hook
  (text-mode . bug-reference-mode)
  (prog-mode . bug-reference-prog-mode)
  :bind
  (:map bug-reference-map
        ("C-c C-o" . bug-reference-push-button))
  :config
  (setq bug-reference-auto-setup-functions nil))

To make this work in a project, a couple of variables need to be set, for example in .dir-locals.el like this:

((nil
  . ((bug-reference-bug-regexp
      . "\\<\\(\\(\\(?:PROJECTA\\|PROJECTB\\)-[[:digit:]]+\\)\\)\\>")
     (bug-reference-url-format
      . "https://tracker.company.example/issue/%s"))))

Org

Default org-mode directory

Set a custom variable for the notes directory, so that it can be referred to later.

(customize-set-variable 'org-directory "~/Documents/Notes/")

Make initial scratch buffer use org-mode

(customize-set-variable 'initial-major-mode 'org-mode)

Editing

Edit src blocks in current window.

(customize-set-variable 'org-src-window-setup 'current-window)

Make indentation and fonts in code blocks work according to mode for the language in the block.

(customize-set-variable 'org-src-tab-acts-natively t)
(customize-set-variable 'org-src-fontify-natively t)

Disable the extra indentation in src blocks.

(customize-set-variable 'org-edit-src-content-indentation 0)

This prevents accidental editing in invisible regions.

(customize-set-variable 'org-catch-invisible-edits 'error)

Shortcut for inserting a block of Elisp.

(add-to-list 'org-structure-template-alist
             '("el" . "src emacs-lisp"))

When trying to edit in an hidden area, expand it before throwing an error.

(customize-set-variable 'org-catch-invisible-edits 'show-and-error)

Display

Enable org-indent mode, which makes org-mode indent sections visually, but not in the saved files.

(customize-set-variable 'org-startup-indented t)

Use variable fonts in org-mode buffers.

(add-hook 'org-mode-hook 'variable-pitch-mode)

Hide the characters surrounding emphasized phrases

(customize-set-variable 'org-hide-emphasis-markers t)

Use real ellipsis character for collapsed subtrees, and prefix it with a space.

(customize-set-variable 'org-ellipsis "…")

Put tags right after headline. This causes fewer conflicts with add-on packages affecting Org-mode style.

(customize-set-variable 'org-tags-column 0)
(customize-set-variable 'org-auto-align-tags nil)

Show Latex-style entities as Unicode characters.

(customize-set-variable 'org-pretty-entities t)

Key bindings

Editing of headers

When point is on a headline, make C-a and C-e go to beginning/end of headline text.

(customize-set-variable 'org-special-ctrl-a/e t)

Insert new headlines after current subtree.

(customize-set-variable 'org-insert-heading-respect-content t)
Navigation between windows in org-mode

Reduce conflict with the global windmove key bindings.

(add-hook 'org-shiftup-final-hook 'windmove-up)
(add-hook 'org-shiftleft-final-hook 'windmove-left)
(add-hook 'org-shiftdown-final-hook 'windmove-down)
(add-hook 'org-shiftright-final-hook 'windmove-right)

Capturing

Add templates for use by org-capture.

(customize-set-variable 'org-capture-templates
      `(("i"
         "Inbox"
         entry
         (file ,(concat org-directory "Inbox.org"))
         "* TODO %?")))

Bind C-c c to org-capture to quickly add notes.

(global-set-key (kbd "C-c c") #'org-capture)

Refiling

This allows refiling within the current buffer, or any agenda files.

(customize-set-variable 'org-refile-targets
                        '((nil :maxlevel . 9)
                          (org-agenda-files :maxlevel . 9)))
(customize-set-variable 'org-outline-path-complete-in-steps nil)
(customize-set-variable 'org-refile-use-outline-path 'file)

Agendas

Search all files in the notes directory when creating agendas.

(customize-set-variable 'org-agenda-files `(,org-directory))

Key binding to open an agenda view.

(global-set-key (kbd "C-c a") #'org-agenda)

Hide done tasks from the agenda.

(customize-set-variable 'org-agenda-skip-scheduled-if-done t)
(customize-set-variable 'org-agenda-skip-deadline-if-done t)

Hide already scheduled tasks from the agenda.

(customize-set-variable 'org-agenda-todo-ignore-scheduled 'all)

Show tags right after headline. Reduces conflicts with packages that affect Org agenda style.

(customize-set-variable 'org-agenda-tags-column 0)

Some agenda visual styling.

(customize-set-variable 'org-agenda-block-separator ?-)
(customize-set-variable 'org-agenda-time-grid
      '((daily today require-timed)
        (800 1000 1200 1400 1600 1800)
        " ┄┄┄" ""))
(customize-set-variable 'org-agenda-current-time-string
      "🠨")

Allow opening links to anchors with org-open-at-point

org-ctags otherwise breaks this functionality if it's loaded, which happens if you open some other filetype that uses ctags.

This is caused by a bug discussed on mailing list here.

(with-eval-after-load 'org-ctags
  (customize-set-variable 'org-open-link-functions nil))

Allow evaluating Shell code blocks in Org

Loading ob-shell will implicitly enable using source blocks in languages supported by shell-mode.

(use-package ob-shell)

Eshell

Disable banner

(customize-set-variable 'eshell-banner-message "")

Aliases

Alias to open file for editing

(defun eshell/e (f) (find-file f))

Add-on packages

Configuration of add-on packages.

Dependencies

These are add-on packages that are dependencies of other packages further down, as listed under each one.

dash

(use-package dash
  :defer)

transient

(use-package transient
  :load-path "site-lisp/transient/lisp")

with-editor

(use-package with-editor
  :load-path "site-lisp/with-editor/lisp")

Interface

fontaine font configuration

This package provides a concise way to define multiple font configurations and switch between them.

(use-package fontaine
  :custom
  (fontaine-presets
   '((light :default-family "Iosevka BKHL Sans Normal"
            :variable-pitch-family "Charis SIL"
            :fixed-pitch-family "Iosevka BKHL Serif Normal")
     (dark :default-family "Iosevka BKHL Sans Normal"
           :variable-pitch-family "Inter BKHL Variable"
           :variable-pitch-weight medium
           :fixed-pitch-family "Iosevka BKHL Sans Normal")
     (t :default-height 130
        :default-weight normal
        :variable-pitch-weight normal
        :fixed-pitch-weight normal))))

modus-themes accessible themes

(use-package modus-themes
  :custom
  (modus-themes-bold-constructs t)
  (modus-themes-italic-constructs t)
  (modus-themes-mixed-fonts t)
  (modus-themes-common-palette-overrides '((fringe unspecified))))

auto-dark to follow desktop dark mode setting

This mode switches Emacs theme triggered by dark style setting of the desktop.

To get this to also change menu bar theme in Fedora, I need to install the gnome-themes-extra package, and the Legacy (GTK3) Theme Scheme Auto Switcher Gnome extension.

(use-package auto-dark
  :demand t
  :custom
  (auto-dark-dark-theme 'modus-vivendi-tritanopia)
  (auto-dark-light-theme 'modus-operandi)
  :config
  (add-hook 'auto-dark-dark-mode-hook
            (defun my/config-dark-mode ()
              (fontaine-set-preset 'dark)))
  (add-hook 'auto-dark-light-mode-hook
            (defun my/config-light-mode ()
              (fontaine-set-preset 'light)))
  (auto-dark-mode))

minions mode line minor mode listing improvements

Hides minor modes in a popup menu to preserve space and make the mode line less noisy.

(use-package minions
  :custom
  (minions-prominent-modes '(trimspace-mode))
  :config
  (minions-mode))

lin mode for highlight of current line.

Enable higlight of current line in selected modes.

(use-package lin
  :custom
  (lin-face 'lin-yellow)
  :config
  (lin-global-mode))

edit-server to edit Firefox text areas

This module provides the server allowing the Edit with Emacs Firefox add-on to open Emacs buffers where you can edit the content of text areas.

(use-package edit-server
  :load-path "site-lisp/edit-server/servers"
  :custom
  (edit-server-new-frame nil)
  :config
  (when (and (daemonp)
             (not (process-status "edit-server")))
    (edit-server-start)))

sv-kalender Swedish calendar localization

(use-package sv-kalender)

vertico for minibuffer completion

This is a library for completion in the minibuffer, which integrates with the emacs completing-read functionality.

(use-package vertico
  :config
  (vertico-mode))

Do not allow the cursor in the minibuffer prompt.

(customize-set-variable 'minibuffer-prompt-properties
                        '(read-only t
                          cursor-intangible t
                          face minibuffer-prompt))
(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)

Add prompt indicator to completing-read-multiple.

(defun my/crm-indicator (args)
  (cons (format "[CRM %s] %s"
                (replace-regexp-in-string
                 (rx (or (seq bos "["
                              (*? nonl)
                              "]*")
                         (seq "["
                              (*? nonl)
                              "]*" eos)))
                 ""
                 crm-separator)
                (car args))
        (cdr args)))
(advice-add #'completing-read-multiple :filter-args #'my/crm-indicator)
(customize-set-variable 'read-extended-command-predicate
                        #'command-completion-default-include-p)

Allow minibuffer commands while in the minibuffer.

(customize-set-variable 'enable-recursive-minibuffers t)

marginalia minibuffer annotations

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

consult search and navigation commands

(use-package consult
  :make-autoloads
  :bind (;; C-c bindings (mode-specific-map)
         ("C-c h" . consult-history)
         ("C-c m" . consult-mode-command)
         ("C-c k" . consult-kmacro)

         ;; C-x bindings (ctl-x-map)
         ("C-x M-:" . consult-complex-command)  ;; replaces `nrepeat-complex-command'
         ("C-x b" . consult-buffer)  ;; replaces `switch-to-buffer'
         ("C-x 4 b" . consult-buffer-other-window)  ;; replaces `switch-to-buffer-other-window'
         ("C-x 5 b" . consult-buffer-other-frame) ;; replaces `switch-to-buffer-other-frame'
         ("C-x r b" . consult-bookmark)  ;; replaces `bookmark-jump'
         ("C-x p b" . consult-project-buffer)  ;; replaces `project-switch-to-buffer'

         ;; Custom bindings for quick register access
         ("M-\"" . consult-register-store)
         ("M-'" . consult-register-load)  ;; replaces `abbrev-prefix-mark' (unrelated)

         ;; Other custom bindings
         ("M-y" . consult-yank-pop)  ;; replaces `yank-pop'

         ;; M-g bindings (goto-map)
         ("M-g e" . consult-compile-error)
         ("M-g f" . consult-flymake)
         ("M-g g" . consult-goto-line)  ;; replaces `goto-line'
         ("M-g M-g" . consult-goto-line)  ;; replaces `goto-line'
         ("M-g o" . consult-outline)
         ("M-g a" . consult-org-agenda)
         ("M-g h" . consult-org-heading)
         ("M-g m" . consult-mark)
         ("M-g k" . consult-global-mark)
         ("M-g i" . consult-imenu)
         ("M-g I" . consult-imenu-multi)

         ;; M-s bindings (search-map)
         ("M-s d" . consult-find)
         ("M-s D" . consult-locate)
         ("M-s g" . consult-grep)
         ("M-s G" . consult-git-grep)
         ("M-s l" . consult-line)
         ("M-s L" . consult-line-multi)
         ("M-s k" . consult-keep-lines)
         ("M-s u" . consult-focus-lines)
         ("M-s '" . consult-register)

         ;; Isearch integration
         ("M-s e" . consult-isearch-history)
         :map isearch-mode-map
         ("M-e" . consult-isearch-history)  ;; replaces isearch-edit-string
         ("M-s e" . consult-isearch-history)  ;; replaces isearch-edit-string
         ("M-s l" . consult-line)  ;; needed by consult-line to detect isearch
         ("M-s L" . consult-line-multi)  ;; needed by consult-line to detect isearch

         ;; Minibuffer history
         :map minibuffer-local-map
         ("M-s" . consult-history)  ;; replaces next-matching-history-element
         ("M-r" . consult-history))  ;; replaces previous-matching-history-element
  :init
  (customize-set-variable 'register-preview-delay 0.5)
  (customize-set-variable 'register-preview-function
                          #'consult-register-format)
  (advice-add #'register-preview :override #'consult-register-window)
  (customize-set-variable 'xref-show-xrefs-function #'consult-xref)
  (customize-set-variable 'xref-show-definitions-function #'consult-xref)
  :config
  (customize-set-variable 'consult-narrow-key "<"))

embark contextual actions

Embark provides ways to trigger commands based on the entity at point or the region, in both regular buffers and minibuffers.

(use-package embark
  :bind
  (("C-." . embark-act)
   ("C-;" . embark-dwim)
   ("C-h B" . embark-bindings)) ;; replaces `describe-bindings'
  :commands embark-prefix-help-command
  :init
  (setq prefix-help-command #'embark-prefix-help-command)
  :config
  ;; Hide the mode line of the Embark live/completions buffers
  (add-to-list 'display-buffer-alist
               `(,(rx bos
                      "*Embark Collect "
                      (or "Live" "Completions")
                      "*")
                 nil
                 (window-parameters (mode-line-format . none)))))

This adds some extra integration between Embark and Consult.

(use-package embark-consult
  :after (embark consult)
  :demand t
  :hook
  (embark-collect-mode . consult-preview-at-point-mode))

pulsar to temporarily highlight current line

Press to flash line of point where the currently active cursor is.

The M-p binding conflicts with my custom binding for flymake-mode, but we can then use M-P as long as that is free.

(use-package pulsar
  :demand t
  :custom
  (pulsar-face 'pulsar-yellow)
  :bind
  (("M-p" . pulsar-pulse-line))
  :hook
  (next-error . pulsar-pulse-line)
  :config
  (push 'next-error pulsar-pulse-functions)
  (pulsar-global-mode))

free-keys to show free bindings

This package can show available bindings in the current buffer.

(use-package free-keys
  :commands free-keys)

Text editing

substitute text replacement commands

(use-package substitute
  :custom
  (substitute-highlight t)
  :bind
  (("M-# s" . substitute-target-below-point)
   ("M-# r" . substitute-target-above-point)
   ("M-# d" . substitute-target-in-defun)
   ("M-# b" . substitute-target-in-buffer)))

trimspace-mode for trimming trailing spaces and newlines

trimspace-mode sets things up so that when a file is opened, it enables deleting trailing whitespace and newlines before saving the file, unless the file when first opened already has traling whitespace of each type.

(use-package trimspace-mode
  :hook
  (prog-mode . trimspace-mode-maybe)
  (text-mode . trimspace-mode-maybe))

whole-line-or-region

This module allows a number of functions to operate on the current line if no region is selected.

(use-package whole-line-or-region
  :config
  (whole-line-or-region-global-mode))

Completion

corfu for completion at point
(use-package corfu
  :demand t
  :bind
  (("C-<tab>" . complete-symbol))
  :config
  (global-corfu-mode))
cape completion at point extensions
(use-package cape
  :make-autoloads
  :bind (("C-c p p" . completion-at-point)
         ("C-c p t" . complete-tag)
         ("C-c p d" . cape-dabbrev)
         ("C-c p h" . cape-hist)
         ("C-c p f" . cape-file)
         ("C-c p k" . cape-keyword)
         ("C-c p s" . cape-symbol)
         ("C-c p a" . cape-abbrev)
         ("C-c p i" . cape-ispell)
         ("C-c p l" . cape-line)
         ("C-c p w" . cape-dict)
         ("C-c p \\" . cape-tex)
         ("C-c p _" . cape-tex)
         ("C-c p ^" . cape-tex)
         ("C-c p &" . cape-sgml)
         ("C-c p r" . cape-rfc1345))
  :init
  (add-to-list 'completion-at-point-functions #'cape-dabbrev)
  (add-to-list 'completion-at-point-functions #'cape-file))
orderless completion style

Orderless provides a completion style that allows typing components of a canditate out of order.

(use-package orderless
  :custom
  (completition-styles '(orderless basic))
  (completion-category-defaults nil)
  (completion-category-overrides '((file (styles partial-completion))))
  :config
  (let ((hook (defun my/minibuffer-setup ()
                (setq-local completion-styles '(orderless basic)))))
    (remove-hook 'minibuffer-setup-hook hook)
    (add-hook 'minibuffer-setup-hook hook 1)))

Programming

fancy-compilation to improve compilation output buffers

(use-package fancy-compilation
  :custom
  (fancy-compilation-override-colors nil)
  :config
  (fancy-compilation-mode))

Languages

Emacs Lisp
package-lint Emacs package linter
(use-package package-lint
  :commands (package-lint-buffer
             package-lint-current-buffer))
xr reverse rx

Helpful functions for refactoring regular expressions to rx expressions.

(use-package xr
  :commands (xr
             xr-pp
             xr-lint
             xr-skip-set
             xr-skip-set-pp
             xr-skip-set-lint
             xr-pp-rx-to-str))
lua-mode
(use-package lua-mode
  :mode (rx ".lua" eos)
  :custom
  (lua-indent-level 4))

Writing

Jinx spell checking

(use-package jinx
  :custom
  (jinx-languages "sv_SE en_GB en_US th_TH")
  :config
  (add-to-list 'jinx--syntax-overrides '(?: . "."))
  :bind
  ("M-$" . jinx-correct)
  :commands (jinx-mode
             global-jinx-mode
             jinx-correct))

markdown-mode for Markdown support

(use-package markdown-mode
  :mode (rx ".md" eos))

olivetti to adjust margins of text

A minor mode that automatically adjusts margins &c. for reading and writing prose.

(use-package olivetti
  :custom
  (olivetti-style nil)
  :hook
  (Info-mode . olivetti-mode)
  (org-mode . olivetti-mode)
  (ewww . olivetti-mode))

File formats

YAML

yaml-mode
(use-package yaml-mode
  :mode (rx ".y" (opt "a") "ml" eos))
flymake-yamllint
(use-package flymake-yamllint)

To use this in a project a .dir-locals.el file is needed, looking something like this:

((yaml-mode
  . ((flymake-yamllint-program . "podman")
     (flymake-yamllint-arguments
      . ("run" "--rm" "--interactive" "docker.io/cytopia/yamllint"))
     (eval . (progn (flymake-yamllint-setup)
                    (flymake-mode))))))

Version control

Magit Git interface

(use-package magit
  :load-path "site-lisp/magit/lisp"
  :bind
  ("C-x g" . magit-status)

  :commands
  magit-call-git

  :custom
  (magit-push-always-verify nil)
  (git-commit-summary-max-length 50)

  :config
  (transient-append-suffix 'magit-push "-t"
    '(4
      "-s"
      "Skip pipeline"
      "-o ci.skip"))
  (transient-append-suffix 'magit-push "-s"
    '(4
      "-m"
      "Create merge request"
      "-o merge_request.create"))
  (transient-append-suffix 'magit-push "-m"
    '(4
      "-M"
      "Create merge request with target"
      "-o merge_request.create -o merge_request.target="))
  (transient-append-suffix 'magit-push "-M"
    '(4
      "-l"
      "Set all tests label"
      "-o merge_request.unlabel=test::skip -o merge_request.label=test::all"))
  (transient-append-suffix 'magit-push "-l"
    '(4
      "-L"
      "Set skip tests label"
      "-o merge_request.unlabel=test::all \
-o merge_request.label=test::skip")))
Automatic commit on save

Function to do automatic commit on save in certain repos. This is for use with for example org-mode, to enable finding things after accidental changes.

(defun my/magic-commit-current-buffer ()
  (magit-call-git "add" buffer-file-name)
  (magit-call-git "commit"
                  "-m"
                  (format "Automatic commit on save of %s"
                          buffer-file-name))
  (magit-refresh))

To use this as an after-save-hook in a project, create a .dir-locals.el with something like this:

((org-mode . ((eval . (add-hook
                       'after-save-hook
                       'my/magic-commit-current-buffer
                       nil t)))))

diff-hl to show uncommitted changes in gutter

Shows changes that are not committed to the version control system for the file open in a buffer in the gutter.

(use-package diff-hl
  :custom
  (diff-hl-draw-borders nil)
  :config
  (global-diff-hl-mode)
  (add-hook 'magit-pre-refresh-hook
            'diff-hl-magit-pre-refresh)
  (add-hook 'magit-post-refresh-hook
            'diff-hl-magit-post-refresh))

(use-package diff-hl-flydiff
  :config
  (diff-hl-flydiff-mode))

git-timemachine file history browsing

(use-package git-timemachine
  :commands git-timemachine)

Org

org-modern styling for Org mode

(use-package org-modern
  :config
  (dolist (face '(org-modern-symbol org-modern-label))
    (set-face-attribute face nil :family "Iosevka BKHL Sans Normal"))
  (global-org-modern-mode))