hiecaq / guix-config

My literate configuration for Guix

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

hiecaq’s guix configuration

Table of Contents

Introduction

This is my all-in-one Guix configuration, working in progress. This aims to eventually replace and deprecate my dotfiles, which has too many historical burdens.

Unless explicitly stated, all code in this configuration is under GPL3 license.

Home Configuration

This is the main entry point for guix home. It can be tested with

guix home -L build container build/home-configuration.scm

and deployed with

guix home -L build reconfigure build/home-configuration.scm
(use-modules
 (gnu home)
 (gnu services)
 (gnu packages)
 <<home-module>>
 )

(home-environment
 <<home-environment-conf>>
 (services
  (append
   <<home-environment-service>>
   )))

This basically reads the default essential service list, and modifies it as needed. home-environment-default-essential-services is private, so we have to use @@ syntax to force importing it. Maybe there is a better way.

(essential-services
 (fonts:modify-essential-service
  ((@@(gnu home) home-environment-default-essential-services)
   this-home-environment)))

This is a list of packages that are not installed by services. Eventually this list should be empty.

(packages (specifications->packages
           (list
            "neovim"
            "guile"
            )))

Guix

This file defines those settings related to Guix itself.

(define-module (hiecaq home guix)
  #:use-module (gnu services)
  #:use-module (gnu packages)
  #:use-module (gnu home services)
  #:use-module (gnu home services guix)
  #:use-module (guix channels))

(define-public services
  (list
   <<guix-service>>
   (simple-service
    'variant-packages-service
    home-channels-service-type
    (list
     <<guix-channel>>
     ))))

Add this module and its services:

((hiecaq home guix) #:prefix guix:)
guix:services

Locales

Set the locales as recommended in the manual.

(service
 (service-type
  (name 'home-locale)
  (extensions
   (list
    (service-extension
     home-profile-service-type
     (const (list
             (specification->package
              "glibc-locales"))))
    (service-extension
     home-environment-variables-service-type
     (const '(("GUIX_LOCPATH" . "${GUIX_PROFILE}/lib/locale"))))))
  (default-value #f)
  (description #f)))

Channels

Cross-Desktop Group (XDG)

This section defines those settings related to the XDG specifications.

(define-module (hiecaq home xdg)
  #:use-module (gnu services)
  #:use-module (gnu packages)
  #:use-module (gnu home services)
  #:use-module (gnu home services xdg)
  #:use-module (guix channels))

(define-public services
  (list
   <<xdg-service>>
   ))

Add this module and its services:

((hiecaq home xdg) #:prefix xdg:)
xdg:services

Base Directories

See Enviroment Variables chapter in latest XDG Base Directory Specification for the description on their purposes.

Guix home instantiate it by default, so technically there is no configuration needed, unless we want to modify their values.

Note that their values are set in $GUIX_HOME/setup-environment, which should be run by $HOME/.profile, which is sourced at the beginning of a login shell.

User Directories

As declared in xdg-user-dirs, this defines “well known” user directories, and their localization.

(simple-service
 'xdg-user-directories-config-service
 home-xdg-user-directories-service-type
 (home-xdg-user-directories-configuration
  (desktop     "$HOME/desktop")
  (documents   "$HOME/documents")
  (download    "$HOME/downloads")
  (music       "$HOME/music")
  (pictures    "$HOME/pictures")
  (publicshare "$HOME/public")
  (templates   "$HOME/templates")
  (videos      "$HOME/videos")))

Shells

(define-module (hiecaq home shell)
  #:use-module (gnu home)
  #:use-module (gnu services)
  #:use-module (gnu packages)
  #:use-module (gnu home services)
  #:use-module (guix channels)
  #:use-module (gnu home services guix)
  #:use-module (gnu home services shells)
  #:use-module (gnu home-services shellutils)
  #:use-module (guix gexp))

TODO: I should split this out later.

(define-public services
  (list
   (simple-service
    'extend-environment-variables
    home-environment-variables-service-type
    `(("PS1" . "$ ")
      ("MANPAGER" . "nvim +Man!")
      ("MANWIDTH" . "80")
      ("QT_AUTO_SCREEN_SCALE_FACTOR" . "1")
      ("RUSTUP_UPDATE_ROOT" . "https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup")
      ("RUSTUP_DIST_SERVER" . "https://mirrors.tuna.tsinghua.edu.cn/rustup")))
   <<shell-service>>
   ))

Add this module and its services:

((hiecaq home shell) #:prefix shell:)
shell:services

Fish

I use fish as a backup interactive-use-only shell.
(service
 home-fish-service-type)

Tools

There are many tools that enhance the command line user experience.

Certificates

See the Guix documentation for details on the CA settings. TODO: Maybe this should be in a higher-level heading?

(service
 (service-type
  (name 'home-certs)
  (extensions
   (list
    (service-extension
     home-profile-service-type
     (const (list
             (specification->package
              "nss-certs"))))
    (service-extension
     home-environment-variables-service-type
     (const '(("SSL_CERT_DIR" . "$HOME/.guix-home/profile/etc/ssl/certs")
              ("SSL_CERT_FILE" . "$SSL_CERT_DIR/ca-certificates.crt")
              ("GIT_SSL_CAINFO" . "$SSL_CERT_FILE")
              ("CURL_CA_BUNDLE" . "$SSL_CERT_FILE"))))))
  (default-value #f)
  (description #f)))

bat

Add bat, which is a cat clone with colors.

(service
 (service-type
  (name 'home-bat)
  (extensions
   (list
    (service-extension
     home-profile-service-type
     (const (list
             (specification->package
              "bat"))))
    (service-extension
     home-environment-variables-service-type
     (const '(("BAT_THEME" . "TwoDark"))))))
  (default-value #f)
  (description #f)))

eza

eza is a community-revived fork of exa, which is “a modern replacement for ls”.

(service
 (service-type
  (name 'home-eza)
  (extensions
   (list
    (service-extension
     home-profile-service-type
     (const (list
             (specification->package
              "eza"))))
    (service-extension
     home-environment-variables-service-type
     (const '(("EZA_COLORS" .
               "*.zip=0:*.gz=0:*.rar=0:*.tar=0:*.7z=0:ex=31:di=244;1"))))))
  (default-value #f)
  (description #f)))

ripgrep

Add ripgrep, which is “a line-oriented search tool that recursively searches the current directory for a regex pattern”. In other words, it is a modern grep.

(simple-service
 'home-ripgrep
 home-profile-service-type
 (list
  (specification->package
   "ripgrep")))

fd

Add fd, which is “a simple, fast and user-friendly alternative to ‘find’”.

(simple-service
 'home-fd
 home-profile-service-type
 (list
  (specification->package
   "fd")))

Direnv

direnv is the environment switcher on the shell level, based on current directories.

(simple-service
 'home-direnv
 home-profile-service-type
 (list
  (specification->package
   "direnv")))

Aliases

And the aliases that I’m using:

alias v="nvim"
alias e="emacsclient -c --no-wait"
alias g="git"
alias ls="exa"
alias l="exa --git-ignore"
alias l.="ls -lah"
alias gc="git commit -v"

Fonts

This file describe how fonts are configured.

(define-module (hiecaq home fonts)
  #:use-module (gnu services)
  #:use-module (gnu home services)
  #:use-module (gnu packages fonts)
  #:use-module (gnu packages fontutils)
  #:use-module (guix gexp)
  #:use-module ((gnu home services fontutils) #:prefix fontutils:))

The home-fontconfig-service-type from vanilla guix comes with a fonts.conf that is literately inconfigurable, so we have to overwrite it. SIDE NOTES: I cannot use @@ to import regenerate-font-cache-gexp from (gnu home services fontutils) I have totally no idea why.

(define (add-fontconfig-config-file he-symlink-path)
  `(("fontconfig/fonts.conf"
     ,(local-file "../../fonts.conf"))))

(define (regenerate-font-cache-gexp _)
  `(("profile/share/fonts"
     ,#~(system* #$(file-append fontconfig "/bin/fc-cache") "-fv"))))

(define home-fontconfig-service-type
  (service-type (name 'home-fontconfig)
                (extensions
                 (list (service-extension
                        home-xdg-configuration-files-service-type
                        add-fontconfig-config-file)
                       (service-extension
                        home-run-on-change-service-type
                        regenerate-font-cache-gexp)
                       (service-extension
                        home-profile-service-type
                        (const (list fontconfig)))))
                (default-value #f)
                (description
                 "Provides configuration file for fontconfig and make
fc-* utilities aware of font packages installed in Guix Home's profile.")))

(define-public (modify-essential-service services)
  `(,@(modify-services
       services
       (delete fontutils:home-fontconfig-service-type))
    ,(service home-fontconfig-service-type)))

Here is the modified fonts.conf:

<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
    <dir>~/.guix-home/profile/share/fonts</dir>
    <alias>
        <family>serif</family>
        <prefer>
            <family>Noto Serif</family>
            <family>Noto Serif CJK SC</family>
            <family>Noto Serif CJK JP</family>
            <family>Noto Serif CJK TC</family>
        </prefer>
    </alias>
    <alias>
        <family>sans-serif</family>
        <prefer>
            <family>Noto Sans</family>
            <family>Noto Sans CJK SC</family>
            <family>Noto Sans CJK JP</family>
            <family>Noto Sans CJK TC</family>
        </prefer>
    </alias>
    <alias>
        <family>monospace</family>
        <prefer>
            <family>Noto Sans Mono</family>
            <family>Noto Sans Mono CJK SC</family>
            <family>Noto Sans Mono CJK JP</family>
            <family>Noto Sans Mono CJK TC</family>
        </prefer>
    </alias>
    <alias>
        <family>emoji</family>
        <prefer>
            <family>Noto Color Emoji</family>
        </prefer>
    </alias>
</fontconfig>

this module simply provides a single service that install the fonts needed.

(define-public services
  (list (simple-service
         'extend-environment-variables
         home-profile-service-type
         (list
          font-hack
          font-google-noto
          font-google-noto-sans-cjk))))
((hiecaq home fonts) #:prefix fonts:)
fonts:services

Emacs

TODO: I’m still not sure if I should put some config as big as Emacs’ in this file.
(define-module (hiecaq home emacs)
  #:use-module (gnu services)
  #:use-module (gnu packages)
  #:use-module (gnu home services)
  #:use-module (guix gexp))

(define-public services
  (list
   <<emacs-service>>))

Add this module and its services:

((hiecaq home emacs) #:prefix emacs:)
emacs:services

Basics

I’m currently using emacs from Guix official channel.

(service
 (service-type
  (name 'home-emacs)
  (extensions
   (list
    (service-extension
     home-profile-service-type
     (const (list
             (specification->package
              "emacs"))))
    (service-extension
     home-xdg-configuration-files-service-type
     (const `(("emacs/init.el" ,(local-file "../../init.el"))
              ("emacs/early-init.el" ,(local-file "../../early-init.el"))
              ("emacs/config/templates.eld" ,(local-file "../../templates.eld")))))
    (service-extension
     home-environment-variables-service-type
     (const '(("EDITOR" . "emacsclient -a nvim -c")
              ("VISUAL" . "emacsclient -a nvim -c"))))))

  (default-value #f)
  (description #f)))

I and my Guix packages definition is at (hiecaq packages emacs-xyz). TODO: makes a channel!

(define-module (hiecaq packages emacs-xyz)
  #:use-module (guix utils)
  #:use-module (guix packages)
  #:use-module (guix git-download)
  #:use-module (guix build utils)
  #:use-module (guix build-system emacs)
  #:use-module (gnu packages)
  #:use-module ((gnu packages textutils) #:prefix upstream:) ;; for vale
  #:use-module ((gnu packages emacs) #:prefix upstream:)
  #:use-module ((gnu packages emacs-xyz) #:prefix upstream:)
  #:use-module ((guix licenses) #:prefix license:))

NOTE: the hash for git-based packages is got by following Guix Cookbook instructions.

Early Initialization

;;; early-init.el --- Configurations before package systems and UI systems -*- lexical-binding: t; buffer-read-only: t; eval: (auto-revert-mode 1) -*-

Packages

I don’t use the built-in package.el to fetch packages, so I’ll turn it off:

(setq package-enable-at-startup nil)

Special Key Remapping

grabbed from [[https://emacsnotes.wordpress.com/2022/09/11/three-bonus-keys-c-i-c-m-and-c-for-your-gui-emacs-all-with-zero-headache/][Three bonus keys—‘C-i’, ‘C-m’ and ‘C-[’—for your GUI Emacs; all with zero headache]]

(add-hook
 'after-make-frame-functions
 (defun setup-blah-keys (frame)
   (with-selected-frame frame
     (when (display-graphic-p)
       (define-key input-decode-map (kbd "C-i") [CTRL-i])
       (define-key input-decode-map (kbd "C-[") [CTRL-lsb]) ; left square bracket
       (define-key input-decode-map (kbd "C-m") [CTRL-m])))))

Some Configurations that might make sense to put here

load prefers the newest version of a file (when suffix is not given).

(setq load-prefer-newer t)
(setq load-no-native t)

Main Configurations

Init file header:

;;; init.el --- Main Configurations -*- lexical-binding: t; buffer-read-only: t; eval: (auto-revert-mode 1) -*-

Use Utf-8 as the default coding system.

(set-language-environment "UTF-8")
(prefer-coding-system 'utf-8-unix)

setup.el

setup.el provides “context sensitive local macros” to “ease repetitive configuration patterns in Emacs”. It is considered as an alternative to the now built-in use-package.

(simple-service
 'home-emacs-setup
 home-profile-service-type
 (list
  (specification->package
   "emacs-setup")))

See Alternative Macro Definer at its Emacs Wiki page, and Michael Fiano’s Emacs Configuration on this. Many of the following tweaks are based on them, with some modifications, mainly for the Emacs 29 changes.

TODO: I should split this out later.

(require 'setup)
(require 'cl-macs)

(defmacro defsetup (name signature &rest body)
  "Shorthand for `setup-define'.
NAME is the name of the local macro.  SIGNATURE is used as the
argument list for FN.  If BODY starts with a string, use this as
the value for :documentation.  Any following keywords are passed
as OPTS to `setup-define'."
  (declare (debug defun))
  (let (opts)
    (when (stringp (car body))
      (setq opts (nconc (list :documentation (pop body))
                        opts)))
    (while (keywordp (car body))
      (let* ((prop (pop body))
             (val `',(pop body)))
        (setq opts (nconc (list prop val) opts))))
    `(setup-define ,name
       (cl-function (lambda ,signature ,@body))
       ,@opts)))

(put #'defsetup 'lisp-indent-function 'defun)
;; use Emacs 29's new `setopt'
(setup-define :option
  (setup-make-setter
   (lambda (name)
     `(funcall (or (get ',name 'custom-get)
                   #'symbol-value)
               ',name))
   (lambda (name val)
     `(setopt ,name ,val)))

  :documentation "Set the option NAME to VAL.
NAME may be a symbol, or a cons-cell.  If NAME is a cons-cell, it
will use the car value to modify the behaviour.  These forms are
supported:

(append VAR)    Assuming VAR designates a list, add VAL as its last
                element, unless it is already member of the list.

(prepend VAR)   Assuming VAR designates a list, add VAL to the
                beginning, unless it is already member of the
                list.

(remove VAR)    Assuming VAR designates a list, remove all instances
                of VAL.

Note that if the value of an option is modified partially by
append, prepend, remove, one should ensure that the default value
has been loaded. Also keep in mind that user options customized
with this macro are not added to the \"user\" theme, and will
therefore not be stored in `custom-set-variables' blocks."
  :debug '(sexp form)
  :repeatable t)

(defsetup :global (&rest body)
  "Use the global keymap for the BODY. This is intended to be used with ':bind'."
  :debug '(sexp)
  (let (bodies)
    (push (setup-bind body (map 'global-map))
          bodies)
    (macroexp-progn (nreverse bodies))))

(defsetup :with-state (state &rest body)
  "Change the evil STATE that BODY will bind to. If STATE is a list, apply BODY
to all elements of STATE. This is intended to be used with ':bind'."
  :indent 1
  :debug '(sexp setup)
  (let (bodies)
    (dolist (state (ensure-list state))
      (push (setup-bind body (state state))
            bodies))
    (macroexp-progn (nreverse bodies))))

(defsetup :bind (key command)
  "Bind KEY to COMMAND in current map, and optionally for current evil states."
  :after-loaded t
  :debug '(form sexp)
  :repeatable t
  (let* ((map (setup-get 'map))
         (global (or (not map) (eq map 'global) (eq map 'global-map)))
         (state (ignore-errors (setup-get 'state))))
    (cond
     ((and state global)
      `(with-eval-after-load 'evil
         (evil-define-key* ',state 'global ,(kbd key) ,command)))
     (state
      `(with-eval-after-load 'evil
         (evil-define-key* ',state ,map ,(kbd key) ,command)))
     (global `(keymap-global-set ,key ,command))
     (t `(keymap-set ,map ,key ,command)))))

(defsetup :unbind (key)
  "Unbind KEY in current map, and optionally for current evil states."
  :after-loaded t
  :debug '(form)
  :repeatable t
  (let* ((map (setup-get 'map))
         (global (or (not map) (eq map 'global) (eq map 'global-map)))
         (state (ignore-errors (setup-get 'state))))
    (cond
     ((and state global)
      `(with-eval-after-load 'evil
         (evil-define-key* ',state 'global ,(kbd key) nil)))
     (state
      `(with-eval-after-load 'evil
         (evil-define-key* ',state ,map ,(kbd key) nil)))
     (global `(keymap-global-unset ,key :remove))
     (t `(keymap-unset ,map ,key :remove)))))

(defsetup :rebind (old-command new-command)
  "Bind NEW-COMMAND to OLD-COMMAND in current map,
and optionally for current evil states."
  :after-loaded t
  :debug '(form sexp)
  :repeatable t
  :ensure (func func)
  (let ((old-command-string
         (cadr (delete "#'" (split-string (format "%s" old-command) "#'")))))
    `(:bind ,(format "<remap> <%s>" old-command-string) ,new-command)))

(defsetup :needs (executable)
  "If EXECUTABLE is not in the path, stop here."
  :debug '(form)
  `(unless (executable-find ,executable)
     ,(setup-quit)))

(defsetup :enable ()
  "Enable the current mode."
  :debug '(form)
  `(,(setup-get 'mode) 1))

Some Sane Configurations

(setup simple
  (:option indent-tabs-mode nil))

(setup frame
  (:option blink-cursor-mode nil))

(setup scroll-bar
  (:option scroll-bar-mode nil))

(setup tool-bar
  (:option tool-bar-mode nil))

(setup menu-bar
  (:option menu-bar-mode nil))

Turn off lockfiles. They cannot be moved to a different directory, and they consistently screw up with file watchers and version control systems. It’d be just easier to turn this feature off.

(setup emacs
  (:option create-lockfiles nil))

4-space indentation:

(setup simple
  (:option tab-width 4))

General programming set up:

(setup prog-mode
  (:hook #'display-line-numbers-mode)
  (:local-set truncate-lines t))

When Emacs writes buffers to files, by the high-level sense it replace the existing file with the content in the buffer. The buffer itself can be backuped, so that if Emacs crashes before the writing, the dirty content can be recovered. How it replaces the content is configurable, and I want to always prefer copying the existing file and then writing the buffer on top of the existing file. See help for details.

(setup files
  (:option make-backup-files nil)
  (:option backup-by-copying t))

Always use y-or-p over yes-or-no, and use read-key instead of read-from-minibuffer. The latter is helpful when using Embark.

(setup emacs
  (:option use-short-answers t
           y-or-n-p-use-read-key t))

I don’t want Emacs to auto-recenter when scrolling off-the-screen:

(setup emacs
  (:option scroll-conservatively 101))

One extra thing: Emacs comes with a customization interface, which supports setting via function calls too (good!) and saves the results in a file (bad!). This snippet set the storage to /dev/null:

(setup cus-edit
  (:option custom-file null-device))

Window Management

(setup window
  (:option switch-to-buffer-obey-display-actions t
           switch-to-buffer-in-dedicated-window 'pop
           ;; left, top, right, bottom
           window-sides-slots '(0 0 1 1))
  (defun fit-window-to-buffer-horiz (window)
    "Fit window to buffer horizontally. Suitable for `window-width'."
    (let ((fit-window-to-buffer-horizontally 'only))
      (fit-window-to-buffer window))))

Universal Argument

I am using Programmer Dvorak (DVP), which swaps digits and special symbols. This makes typing numbers generally inconvenient. The idea behind this change is that we should define const variables to hold these numbers to reduce the chances we need to actually type numbers. However, Emacs (and Evil) use numbers to repeat commands, a situation that we still need typing digits directly. This is improved by the following tweak.

C-u basically invokes the unversal-argument-map transient map, so we can remap the digit row’s symbols to actual digits. Also I add a binding to insert current universal argument’s number.

(defvar my-dvp-digit-row-alist
  '((7 . "[")
    (5 . "{")
    (3 . "}")
    (1 . "(")
    (9 . "=")
    (0 . "*")
    (2 . ")")
    (4 . "+")
    (6 . "]")
    (8 . "!"))
  "`Higher' case characters to digits mapping on dvorak digit row")

(setup simple
  (defun my-digit-argument (digit)
    "Return the command that inputs the given
digit as universal argument."
    (lambda (arg)
      (interactive "P")
      (let ((last-command-event (+ digit ?0)))
        (digit-argument arg))))
  (:with-map universal-argument-map
    (dolist (d (number-sequence 0 9))
      (:bind (alist-get d my-dvp-digit-row-alist)
             (my-digit-argument d)))
    (:bind "<CTRL-i>" (lambda (arg)
                        (interactive "P")
                        (insert (format "%s" arg))))))

Also here is a helper macro for binding commands. I personally do not like using universal argument at all.

(defmacro my-with-universal-argument (cmd)
  "Wrap the given CMD with a lambda that set universal argument before
  interactively calling CMD."
  `(lambda ()
     (interactive)
     (let ((current-prefix-arg '(4)))
       (call-interactively ,cmd))))

PCRE

Emacs comes with an Rx Notation that converts sexp DSL in that format into Emacs Regex strings. However, Emacs’ regex format is a little bit different from PCRE, the most prevalent regex standard among tools outside of Emacs. pcre2el is the missing bridge between PCRE, Emacs regex string and rx notation.

(simple-service
 'home-emacs-setup
 home-profile-service-type
 (list
  (specification->package
   "emacs-pcre2el")))

Help

TODO: this should not require help.

(setup (:require help)
  (:global (:unbind "C-h C-h")))

Xdg

I add a xdg-log-home getter definition. This is technically not part of the standard, but Guix has this environment variable defined, so why not?

(setup (:require xdg)
  (unless (functionp #'xdg-log-home)
    (defun xdg-log-home ()
      "Return the base directory for user-specific log data."
      (xdg--dir-home "XDG_LOG_HOME" "~/.local/log"))))

No Littering

no-littering helps put emacs directory clean, sorting package-created files and directories into reasonable directories. One thing it misses is the distinguishing between permanent data and temporary data. I used to fork it to provide this distinguishing, but it turns out to be too troublesome to maintain. Now I simply consider this as a “fallback” solution. Later on for the variables from packages I really use I’ll overwrite them manually.

(simple-service
 'home-emacs-setup
 home-profile-service-type
 (list
  (specification->package
   "emacs-no-littering")))
(setup (:require no-littering))
(defmacro def-exdg-home-dir (xdg-name)
  (list 'progn
        `(defvar ,(intern (format "exdg-%s-dir" xdg-name))
           (expand-file-name (convert-standard-filename "emacs/") (,(intern (format "xdg-%s-home" xdg-name)))))
        `(defun ,(intern (format "exdg-%s" xdg-name)) (file)
           (expand-file-name (convert-standard-filename file) ,(intern (format "exdg-%s-dir" xdg-name))))))

(def-exdg-home-dir config)
(def-exdg-home-dir cache)
(def-exdg-home-dir data)
(def-exdg-home-dir state)
(def-exdg-home-dir log)

(setq exdg-config-dir (expand-file-name "config/" user-emacs-directory))

Modus Themes

(simple-service
 'home-emacs-modus-themes
 home-profile-service-type
 (list
  (specification->package
   "emacs-modus-themes")))
(setup modus-themes
  (:option modus-themes-mixed-fonts t)
  (:require modus-themes)
  (load-theme 'modus-vivendi :no-confirm))

Mode Line

(defvar-local my-mode-line-format nil
  "My `mode-line-format', for easy toggle between the default version.")

(defun my-toggle-mode-line-format ()
  (interactive)
  (let* ((standard (eval (car (get 'mode-line-format 'standard-value))))
         (new-format (if (eq standard (default-value 'mode-line-format))
                         my-mode-line-format
                       standard)))
    (setq-default mode-line-format new-format)
    (kill-local-variable 'mode-line-format)
    (force-mode-line-update)))

(defun my-mode-line-recursion--indicator ()
  (when-let (((mode-line-window-selected-p))
             (depth (- (recursion-depth) (if (active-minibuffer-window) 1 0)))
             ((> depth 0)))
    (format "R%d" depth)))

(defvar-local my-mode-line-recursion-indicator
    '(:eval (my-mode-line-recursion--indicator)))
(put 'my-mode-line-recursion-indicator 'risky-local-variable t)

(defvar-local my-mode-line-indicators (list my-mode-line-recursion-indicator
                                            '(:eval (when find-file-literally "L "))
                                            '(:eval (when buffer-read-only "RO "))
                                            '(:eval (unless (string-equal (format-mode-line "%@") "-") "Remote "))
                                            '(:eval (when (buffer-narrowed-p) '(:propertize "Narrow " face warning)))
                                            '(:eval (when (window-dedicated-p) "Dedi "))
                                            '(:eval (when (window-parameter (selected-window) 'window-side) "Side "))
                                            '(current-input-method current-input-method-title)
                                            '(god-local-mode "God ")
                                            '(defining-kbd-macro "Def ")
                                            '(flymake-mode flymake-mode-line-format)
                                            '(:eval (when (buffer-modified-p) "M "))
                                            '(:eval (unless (eq evil-state 'normal)
                                                      (string-trim evil-mode-line-tag))))
  "A list of mode line indicators that is displayed on active window.")

(put 'my-mode-line-indicators 'risky-local-variable t)

(setopt my-mode-line-format '("%e"
                              mode-line-front-space
                              nil ;; eshell
                              (:eval (when (mode-line-window-selected-p)
                                       (list my-mode-line-indicators
                                             mode-line-misc-info)))

                              mode-line-format-right-align

                              mode-line-buffer-identification
                              (vc-mode vc-mode)
                              " "
                              mode-name
                              mode-line-end-spaces))

(setopt mode-line-buffer-identification (propertized-buffer-identification "%b"))
(setopt mode-line-format my-mode-line-format)

Midnight

midnight is Emacs’ built-in cron-like service that run once during midnight each day. Its main purpose is to do same maintenance for the Emacs instance, such as cleaning very old unused buffers. It simply invokes midnight-hook (which contains #'clean-buffer-list by default) midnight-delay seconds after the midnight.

(setup midnight
  (:option midnight-delay (* 4 60 60))
  (:enable))

Auto Save

(setup files
  (let ((autosave-dir (exdg-cache "auto-save/")))
    (mkdir autosave-dir t)
    (:option auto-save-file-name-transforms
             `(("\\`/[^/]*\\([^/]*/\\)*\\([^/]*\\)\\'" ,(concat autosave-dir "\\2") t)))))

Recentf

recentf is an Emacs built-in minor mode that saves recent file list.

(setup recentf
  (:option recentf-save-file (exdg-state "recentf-save.el"))
  (:enable))

Save History

savehist is an Emacs built-in minor mode that save minibuffer histories to a file.

(setup savehist
  (:option savehist-file (exdg-state "savehist.el"))
  (:enable))

Editorconfig

editorconfig is a very handy tool that standardize how different editors should behave according to different language, including tab width, trailing space and so on. It is not only helpful for team to maintain a codestyle standard, but also a handful tool for people use several different editors / computers, like I do.

editorconfig-emacs implements its own editorconfig core, so it’s logical to assume that it works on any platform.

(simple-service
 'home-emacs-editorconfig
 home-profile-service-type
 (list
  (specification->package
   "emacs-editorconfig")))
(setup editorconfig
  (:enable))

Envrc

envrc is Emacs’ integration with direnv that works in buffer-local style.

(simple-service
 'home-emacs-envrc
 home-profile-service-type
 (list
  (specification->package
   "emacs-envrc")))
(setup envrc
  (:with-mode envrc-global-mode
    (:hook-into after-init)))

Subword

subword-mode is an Emacs built-in that makes CamelCase be considered as 2 separate words Camel and Case. Evil also respects this minor mode. I’ve found that to turn on this mode is almost always positive for Evil usages, because the io ao text objects select the whole symbol anyway, pretty much covers the non-subword usage. There is also superword-mode BTW. See MixedCase Words and Misc for Programs in the documentation.

(setup subword
  (:hook-into text-mode prog-mode))

Highlight Parentheses

highlight-parentheses, well, highlights parentheses surrounding point.

(define-public emacs-highlight-parentheses
  (let ((version "2.2.2")
        (revision "0")
        (url "https://git.sr.ht/~tsdh/highlight-parentheses.el"))
    (package
      (name "emacs-highlight-parentheses")
      (version version)
      (source
       (origin
         (method git-fetch)
         (uri
          (git-reference
           (url url)
           (commit version)))
         (file-name (git-file-name name version))
         (sha256
          (base32 "0wvhr5gzaxhn9lk36mrw9h4qpdax5kpbhqj44745nvd75g9awpld"))))
      (build-system emacs-build-system)
      (home-page url)
      (synopsis "Highlights parentheses surrounding point in Emacs")
      (description "Highlight-parentheses.el dynamically highlights
the parentheses surrounding point based on nesting-level using configurable
lists of colors, background colors, and other properties.")
      (license license:gpl3))))
(simple-service
 'home-emacs-highlight-parentheses
 home-profile-service-type
 (list
  (specification->package
   "emacs-highlight-parentheses")))

The configs here is basically from Note on highlight-parentheses.el in Modus Themes documentation, modified a little bit.

(setup highlight-parentheses
  (defvar my-highlight-parentheses-use-background t
    "Prefer `highlight-parentheses-background-colors'.")

  (setq my-highlight-parentheses-use-background t) ; Set to nil to disable backgrounds

  (modus-themes-with-colors
    ;; Our preference for setting either background or foreground
    ;; styles, depending on `my-highlight-parentheses-use-background'.
    (if my-highlight-parentheses-use-background

        ;; Here we set color combinations that involve both a background
        ;; and a foreground value.
        (setq highlight-parentheses-background-colors (list bg-cyan-intense
                                                            bg-magenta-intense
                                                            bg-green-intense
                                                            bg-yellow-intense)
              highlight-parentheses-colors (list cyan
                                                 magenta
                                                 green
                                                 yellow))

      ;; And here we pass only foreground colors while disabling any
      ;; backgrounds.
      (setq highlight-parentheses-colors (list green-intense
                                               magenta-intense
                                               blue-intense
                                               red-intense)
            highlight-parentheses-background-colors nil)))
  (:hook-into prog-mode)
  (:with-function highlight-parentheses-minibuffer-setup
    (:hook-into minibuffer-setup)))

Transient

(setup transient
  (:option transient-history-file (exdg-state "transient/history.el")
           transient-levels-file (exdg-state "transient/levels.el")
           transient-values-file (exdg-state "transient/values.el")))

Evil

It’s name tells everything: the Extensible Vi Layer for Emacs, Evil. It works pretty well as a Vim simulation, much better than VsCode’s or Intellij’s. Besides, it is charming combination of Vim’s model-based editing with Emacs’ keymap system, to some extent, as a personal opinion, better than the native Vim on the model-based editing system.

References:

(simple-service
 'home-emacs-evil
 home-profile-service-type
 (specifications->packages
  (list
   "emacs-goto-chg"
   "emacs-evil"
   "emacs-evil-collection-next"
   "emacs-evil-surround"
   "emacs-evil-replace-with-register"
   "emacs-evil-snipe"
   "emacs-evil-commentary")))

I need some latest contributions to the evil-collection repository:

(define-public emacs-evil-collection-next
  (let ((commit "5886bab852dc9e31959e70384d535473e44504ad")
        (last-release-version "0.0.10")
        (revision "0"))
    (package
     (inherit upstream:emacs-evil-collection)
     (name "emacs-evil-collection-next")
     (version (git-version last-release-version revision commit))
     (source
      (origin
       (method git-fetch)
       (uri (git-reference
             (url "https://github.com/emacs-evil/evil-collection")
             (commit commit)))
       (file-name (git-file-name name version))
       (sha256
        (base32
         "0dz9dkmxm4j2r2nilgxwgvsgbm531rrsiszzx480zrmqybdsziq6")))))))
(setup evil
  (:option
   evil-want-integration t ;; require by collection
   evil-want-keybinding nil ;; require by collection
   evil-echo-state nil ;; Don't echo the =<INSERT>= etc info in minibuffer.
   evil-undo-system 'undo-redo ;; Use Emacs 28 new ~undo-redo~ as the undo-redo system
   evil-disable-insert-state-bindings t ;; I don't want to use Vim's insert mode bindings in insert state:
   evil-respect-visual-line-mode t ;; When =visual-line-mode= is set (especially in =org-mode=), I want Vim to behave as visual lines are normal lines (i.e. bind =j= to =gj= etc)
   evil-mode-line-format nil
   evil-search-module 'evil-search)
  (defvar-keymap my-leader-map)
  (defun my-leader-key ()
    (interactive)
    (set-transient-map my-leader-map))
  (:global
   (:unbind "C-SPC")
   ;; (:bind "C-SPC" #'my-leader-key)
   (:bind "C-SPC" (my-with-universal-argument #'embark-act)))
  (:require evil)
  (:enable)
  (:global
   (:with-state (motion insert)
     (:unbind "C-z"))
   (:with-state (normal)
     (:bind "<CTRL-i>" #'evil-jump-forward))))

(setup evil-collection
  (:option evil-collection-setup-minibuffer t
           evil-collection-key-blacklist '("SPC" "C-SPC" "DEL" "C-z"))
  (:require evil-collection)
  (evil-collection-init))

Evil Surround

evil-surround defines operators that change/add/delete delimiters around a text object. I found that its key bindings conflict with evil-snipe a lot, so I remap them to m, which stands for markers.

(setup evil-surround
  (:with-state (operator visual)
    (:unbind "s" "S" "g S"))
  (:with-state (normal operator)
    (:bind "m" #'evil-surround-edit
           "M" #'evil-Surround-edit))
  (:with-state visual
    (:bind "m" #'evil-surround-region
           "M" #'evil-Surround-region))
  (:also-load evil)
  (:with-function turn-on-evil-surround-mode
    (:hook-into prog-mode text-mode wdired-mode comint-mode eshell-mode minibuffer-setup)))

Evil Replace With Register

evil-replace-with-register defines a rplace operator.

(setup (:require evil-replace-with-register)
  (:global (:with-state (normal visual)
             (:bind "," #'evil-replace-with-register))))

Evil Snipe

evil-snipe is a Evil port of Vim’s clever-f and vim-sneak. It currently does not support separating the scope for f/F/t/T from for s/S, which is a little bit annoying.

There is currently a bug in evil-snipe’s type declarations for evil-snipe-scope, so I forked it. Once the PR is merged, I’ll switch back to the upstream version.

(define-public emacs-evil-snipe
  (let ((commit "3ad53b8da0dd23093a3f2f0e5c13ecdb08ba8efa")
        (last-release-version "2.0.8") ;; from the el file version header
        (revision "0")
        (url "https://github.com/hiecaq/evil-snipe"))
    (package
     (name "emacs-evil-snipe")
     (version (git-version last-release-version revision commit))
     (source
      (origin
       (method git-fetch)
       (uri (git-reference
             (url url)
             (commit commit)))
       (file-name (git-file-name name version))
       (sha256
        (base32
         "0fk9nl0h1j1ig6pvb4aix3injxi2jyw9djixchxf4aky11znivgj"))))
     (propagated-inputs
      (list upstream:emacs-evil))
     (build-system emacs-build-system)
     (home-page url)
     (synopsis "2-char searching ala vim-sneak & vim-seek, for evil-mode")
     (description "This library It provides 2-character motions for quickly
(and more accurately) jumping around text, compared to evil's built-in
f/F/t/T motions, incrementally highlighting candidate targets as you type.")
     (license license:expat))))
(setup (:require evil-snipe)
  (:with-function turn-off-evil-snipe-override-mode (:hook-into magit-mode))
  (:option evil-snipe-repeat-scope 'whole-line)
  (:with-map evil-snipe-override-mode-map
    (:with-state (normal motion operator visual)
      (:bind "s" #'evil-avy-goto-char-2
             "S" #'evil-avy-goto-char-2)))
  (:with-mode evil-snipe-override-mode
    (:enable)))

Evil Commentary

evil-commentary defines operators for commenting.

(setup evil-commentary
  (:also-load evil)
  (:enable))

God mode

god-mode provides a minor mode in which modifier keys of key bindings are handled sepecially: C- is not needed any more, M- is implied with a single key, etc.

(simple-service
 'home-emacs-setup
 home-profile-service-type
 (list
  (specification->package
   "emacs-god-mode")))
(setup (:require god-mode)
  (:option god-mode-alist '((nil . "C-") ("m" . "M-") ("M" . "C-M-"))
           god-mode-enable-function-key-translation t)
  (:global
      (:with-state (normal visual motion)
        (:bind "SPC" #'god-execute-with-current-bindings))
    (:with-state (insert emacs motion)
      (:bind "C-<espace>" #'god-execute-with-current-bindings)))
  (defun my-god-mode-lookup-key-sequence (&optional key key-string-so-far)
    "Retry with literal KEY when the non-literal attempt failed."
    (interactive)
    (let ((sanitized-key
           (god-mode-sanitized-key-string
            (or key (read-event key-string-so-far)))))
      (condition-case nil
          (god-mode-lookup-command
           (god-key-string-after-consuming-key sanitized-key key-string-so-far))
        (error (when key-string-so-far
                 (setq god-literal-sequence t)
                 (god-mode-lookup-command
                  (god-key-string-after-consuming-key sanitized-key key-string-so-far)))))))

  (advice-add #'god-mode-lookup-key-sequence :override #'my-god-mode-lookup-key-sequence))

Which key

which-key is a minor mode that hints you the keybindings prefixed with what you have typed when you get stuck.

(simple-service
 'home-emacs-setup
 home-profile-service-type
 (list
  (specification->package
   "emacs-which-key")))
(setup (:require which-key)
  (:option which-key-show-transient-maps t
           which-key-use-C-h-commands nil)
  (which-key-enable-god-mode-support)
  (:enable))

As a side note, which-key default configuration requires there to be at least 1 slot at the bottom in window-sides-slots.

Posframe

posframe pops a child-frame at point, connected to its root window’s buffer.

(simple-service
 'home-emacs-setup
 home-profile-service-type
 (list
  (specification->package
   "emacs-posframe")))

Eldoc

(setup eldoc
  (:option eldoc-documentation-strategy 'eldoc-documentation-compose-eagerly
           (prepend display-buffer-alist) `(,(rx "*eldoc*")
                                            (display-buffer-reuse-mode-window display-buffer-in-direction)
                                            (direction . right)
                                            (window-width . fit-window-to-buffer-horiz)
                                            (body-function . select-window)
                                            (dedicated . t)
                                            (window-parameters . ((mode-line-format . none))))))

eldoc-box shows eldoc in a separate childframe instead of the crowded echo area.

(simple-service
 'home-emacs-eldoc-box
 home-profile-service-type
 (list
  (specification->package
   "emacs-eldoc-box")))
(setup eldoc-box
  (:option eldoc-box-clear-with-C-g t
           eldoc-box-doc-separator
           (concat "\n"
                   (propertize " " 'face 'completions-group-separator
                               'display '(space :align-to right)))
           eldoc-box-max-pixel-width 1600
           eldoc-box-max-pixel-height 1400)
  (:with-function eldoc-box-hover-mode
    (:hook-into text-mode prog-mode))

  (defun my-eldoc-box-quit-frame-when-interactive (interactive)
    """When manually open the doc buffer, close eldoc-box immediately."""
    (when interactive
      (eldoc-box-quit-frame)))
  (advice-add #'eldoc-doc-buffer :before #'my-eldoc-box-quit-frame-when-interactive))

Ace Window

ace-window is helpful to do things the “embark” way: pick a window, then decide what to do with it.

Its package definition in the Guix official channel is for the “latest” release version, which is as old as 2014. So I makes a variation to use the master branch HEAD at the time of writing.

(define-public emacs-ace-window-next
  (let ((commit "77115afc1b0b9f633084cf7479c767988106c196")
        (last-release-version "0.10.0")
        (revision "0"))
    (package
     (inherit upstream:emacs-ace-window)
     (name "emacs-ace-window-next")
     (version (git-version last-release-version revision commit))
     (source
      (origin
       (method git-fetch)
       (uri (git-reference
             (url "https://github.com/abo-abo/ace-window")
             (commit commit)))
       (file-name (git-file-name name version))
       (sha256
        (base32
         "1l6rp92q4crahx9nq7s6zxqyw7ccrhkl95v70vxra7zndqpqwsbq")))))))
(simple-service
 'home-emacs-ace-window
 home-profile-service-type
 (list
  (specification->package
   "emacs-ace-window-next")))
(setup (:require ace-window)
  (:option aw-keys '(?u ?h ?e ?t ?i ?d ?o ?n ?a ?s)
           aw-translate-char-function (lambda (c)
                                        (pcase c
                                          (?\[ ?7)
                                          (?\{ ?5)
                                          (?\} ?3)
                                          (?\( ?1)
                                          (?= ?9)
                                          (?* ?0)
                                          (?\) ?2)
                                          (?+ ?4)
                                          (?\] ?6)
                                          (?! ?8)
                                          (_ c)))
           aw-dispatch-alist '((?Q aw-delete-window "Delete Window")
                               (?W aw-swap-window "Swap Windows")
                               (?M aw-move-window "Move Window")
                               (?C aw-copy-window "Copy Window")
                               (?J aw-switch-buffer-in-window "Select Buffer")
                               (?D aw-use-frame "Make frame for window")
                               (?N aw-flip-window)
                               (?U aw-switch-buffer-other-window "Switch Buffer Other Window")
                               (?E aw-execute-command-other-window "Execute Command Other Window")
                               (?F aw-split-window-fair "Split Fair Window")
                               (?S aw-split-window-vert "Split horizontally")
                               (?V aw-split-window-horz "Split vertically")
                               (?O delete-other-windows "Delete Other Windows")
                               (?T aw-transpose-frame "Transpose Frame")
                               ;; ?i ?r ?t are used by hyperbole.el
                               (?? aw-show-dispatch-help)))
  (:global (:rebind #'evil-window-next #'ace-window
                    #'other-window  #'ace-window)))

ace-window has its posframe integration now (which is the main reason why I need more recent commits), which use it to show the keys in the centers of buffers.

(setup ace-window-posframe
  (:enable))

Spell Checking

See the documentation for details.

Emacs comes with a spell checking wrapper…

(setup ispell
  (:needs "hunspell")
  (:option ispell-program-name "hunspell"))

… and an on-the-fly spell checker(which uses ispell as the backend).

(setup flyspell
  (:needs "hunspell")
  ;; (general-unbind flyspell-mode-map "C-;")
  (:unbind "C-;")
  (:hook-into text-mode)
  (:with-mode flyspell-prog-mode
    (:hook-into prog-mode)))

Flyspell Correct

The default UI for ispell is quite hard to use, and there is a package flyspell-correct that makes use of the completing-read interface to make things much more usable.

Note that the version in official Guix Package Channel is 0.6.1, which was 3 years ago. It is kind of broken on my site, so I’ll use the master HEAD version instead:

(simple-service
 'home-emacs-flyspell-correct
 home-profile-service-type
 (list
  (specification->package
   "hunspell")
  (specification->package
   "hunspell-dict-en-us")
  (specification->package
   "emacs-flyspell-correct-next")))

I drop the unused dependencies. It is ridiculous to have to propagate ivy, helm and popup to use this package.

(define-public emacs-flyspell-correct-next
  (let ((commit "7d7b6b01188bd28e20a13736ac9f36c3367bd16e")
        (last-release-version "0.6.1")
        (revision "0"))
    (package
     (inherit upstream:emacs-flyspell-correct)
     (name "emacs-flyspell-correct-next")
     (arguments
      `(#:exclude '("flyspell-correct-.*\\.el")))
     (propagated-inputs (list))
     (version (git-version last-release-version revision commit))
     (source
      (origin
       (method git-fetch)
       (uri (git-reference
             (url "https://github.com/d12frosted/flyspell-correct")
             (commit commit)))
       (file-name (git-file-name name version))
       (sha256
        (base32
         "1b6h3wjmxg9d1d3mfvw6fsgkr1w0d14zxllv9jb5cscl5lq8rbmm")))))))
(setup (:require flyspell-correct)
  (:needs "hunspell")
  (:also-load flyspell)
  (:global (:rebind #'ispell-word #'flyspell-correct-wrapper)))

Xref

xref is an Emacs built-in cross referencing browsing package.

This file provides a somewhat generic infrastructure for cross referencing commands, in particular “find-definition”.

(setup xref
  (:option xref-search-program 'ripgrep)
  (:global (:with-state (normal)
             (:bind "g r" #'xref-find-references))))

Topsy

topsy shows a sticky header at the top of the window, displaying which function is the one that extends to the lines before the top of the displayed buffer.

(define-public emacs-topsy
  (let ((commit "8ae0976dfdbe4461c33ed44cf1dedc2c903b0bb0")
        (last-release-version "0.1-pre") ;; from the el file version header
        (revision "0")
        (url "https://github.com/alphapapa/topsy.el"))
    (package
     (name "emacs-topsy")
     (version (git-version last-release-version revision commit))
     (source
      (origin
       (method git-fetch)
       (uri (git-reference
             (url url)
             (commit commit)))
       (file-name (git-file-name name version))
       (sha256
        (base32
         "032i1prl2v5w4l37zjlqam7063s56nk61nj5l3ypmxp98yz9nrq8"))))
     (build-system emacs-build-system)
     (home-page url)
     (synopsis "Simple sticky header showing definition beyond top of window")
     (description "This library shows a sticky header at the top of the window.
The header shows which definition the top line of the window is within. ")
     (license license:gpl3))))

Although topsy recommends to use org-sticky-header instead, this snippet for org-mode is good enough for me:

(setup topsy
  (with-eval-after-load 'topsy
    (:option (prepend topsy-mode-functions)
            '(org-mode . (lambda ()
                            (save-excursion
                                (goto-char (window-start))
                                (when (org-at-heading-p)
                                (forward-line -1))
                                (org-get-heading))))))
  (:hook-into prog-mode org-mode))
(simple-service
 'home-emacs-topsy
 home-profile-service-type
 (list
  (specification->package
   "emacs-topsy")))

Orderless

orderless add space-separated component (which then matches against several matching styles) completion style to minibuffer and other completion UI.

(simple-service
 'home-emacs-orderless
 home-profile-service-type
 (list
  (specification->package
   "emacs-orderless")))

Orderless needs some hack to work with consult-buffer and friends. Steal from minad’s:

(setup orderless
  (defun +orderless--consult-suffix ()
    "Regexp which matches the end of string with Consult tofu support."
    (if (and (boundp 'consult--tofu-char) (boundp 'consult--tofu-range))
        (format "[%c-%c]*$"
                consult--tofu-char
                (+ consult--tofu-char consult--tofu-range -1))
      "$"))

  ;; Recognizes the following patterns:
  ;; * .ext (file extension)
  ;; * regexp$ (regexp matching at end)
  (defun +orderless-consult-dispatch (word _index _total)
    (cond
     ;; Ensure that $ works with Consult commands, which add disambiguation suffixes
     ((string-suffix-p "$" word)
      `(orderless-regexp . ,(concat (substring word 0 -1) (+orderless--consult-suffix))))
     ;; File extensions
     ((and (or minibuffer-completing-file-name
               (derived-mode-p 'eshell-mode))
           (string-match-p "\\`\\.." word))
      `(orderless-regexp . ,(concat "\\." (substring word 1) (+orderless--consult-suffix)))))))

Sometimes it can be useful to use rx-notation directly.

(setup orderless
  (defun my-orderless-rx (component)
    "Match a component as rx-notation."
    (when-let ((m (ignore-errors (read-from-string component)))
               (form (car m))
               (regex (ignore-errors (rx-to-string form)))
               ((= (length component) (cdr m))))
      regex)))

For a normal orderless matching, which is triggered when completion-styles triggers orderless, it use a chain of responsibility to decide which matcher to use. Essentially, matchers are either

  • grouped in dispatchers (listed in orderless-style-dispatchers, each is also a chain of responsibility itself), or
  • listed directly in orderless-matching-styles, which is basically the catch-all dispatcher at the end of the chain.
(setup orderless
  (:option orderless-style-dispatchers '(+orderless-consult-dispatch
                                         orderless-kwd-dispatch
                                         orderless-affix-dispatch)
           orderless-matching-styles '(orderless-regexp)))

Affix dispatcher can be adjust by setting the orderless-affix-dispatch-alist, which maps the single affix character to matcher.

(setup orderless
  (with-eval-after-load 'orderless
    (:option (prepend orderless-affix-dispatch-alist) `(?_ . ,#'my-orderless-rx)
             (prepend orderless-affix-dispatch-alist) `(?- . ,#'orderless-prefixes))))

Note that file no longer needs special treat for recent Emacs and Tramp, see here.

Finally, define how the completion system actually works. Minad states in the above notes that

Note that completion-category-overrides is not really an override, but rather prepended to the default completion-styles.

(setup minibuffer
  (:option completion-category-defaults nil)
  (:option completion-styles '(orderless basic)
           completion-category-overrides '((file (styles partial-completion)))))

We can also defines our own completion style as used in completion-styles etc, with the help of orderless.

(setup orderless
  (with-eval-after-load 'orderless
    (orderless-define-completion-style orderless-only-initialism
      (orderless-matching-styles '(orderless-initialism)))))

My orderless seperator is toggle-able. It defaults to orderless-escapable-split-on-space, but in cases it is possible to switch to use escaped space only. For example, it becomes handy when using my-orderless-rx.

(setup orderless
  (defvar my-orderless-seperator-use-escaped-space nil
    "Use escaped space in orderless component separation.")

  (defun my-orderless-seperator-toggle ()
    "Toggle the value of `my-orderless-seperator-use-escaped-space' locally"
    (interactive)
    (setq-local my-orderless-seperator-use-escaped-space
                (not my-orderless-seperator-use-escaped-space))
    (message "use-escaped-space: [%s]" my-orderless-seperator-use-escaped-space))

  (defun my-orderless-component-separator (string)
    "Default to `orderless-escapable-split-on-space',
but switchable to based on literal spaces."
    (if my-orderless-seperator-use-escaped-space
        (split-string string  "\\\\ " t)
      (orderless-escapable-split-on-space string)))

  (:option orderless-component-separator #'my-orderless-component-separator))

Vertico

vertico “provides a performant and minimalistic vertical completion UI based on the default completion system.”

(simple-service
 'home-emacs-setup
 home-profile-service-type
 (list
  (specification->package
   "emacs-vertico")))

By default, C-b allows the cursor to moves onto the prompt, which is not good because the prompt is read-only and many commands just don’t work once you do that. On the README of vertico the author provides the following hack, utilizing cursor-intangible-mode:

(setup cursor-sensor
  (:option minibuffer-prompt-properties
           '(read-only t cursor-intangible t face minibuffer-prompt))
  (:with-mode cursor-intangible-mode
    (:hook-into minibuffer-setup)))
(setup (:require vertico)
  (:option enable-recursive-minibuffers t)
  (:with-map vertico-map
    (:rebind #'evil-goto-first-line #'vertico-first
             #'evil-goto-line #'vertico-last
             #'evil-scroll-page-down #'vertico-scroll-up
             #'evil-scroll-page-up #'vertico-scroll-down)
    (:bind "C-'" #'my-orderless-seperator-toggle))
  (:with-mode vertico-multiform-mode
    (:enable))
  (:enable))

Marginalia

marginalia adds info to the right of completion candidates, thus the name margin-alia.

(simple-service
 'home-emacs-setup
 home-profile-service-type
 (list
  (specification->package
   "emacs-marginalia")))
(setup (:require marginalia)
  (:enable))

Consult

consult provides practical commands based on the Emacs completion function completing-read. What this means is that basically consult pop up candidates when calling its commands into comleting-read.

(simple-service
 'home-emacs-setup
 home-profile-service-type
 (list
  (specification->package
   "emacs-consult")))
(setup (:require consult)
  (:option consult-preview-key "C-j"
           xref-show-definitions-function #'consult-xref
           xref-show-xrefs-function #'consult-xref
           consult-locate-args "locate --ignore-case --regex")
  ;; from https://github.com/minad/consult/wiki#consult-ripgrep-or-line-counsel-grep-or-swiper-equivalent
  (defcustom my-consult-ripgrep-or-line-limit 300000
    "Buffer size threshold for `my-consult-ripgrep-or-line'.
When the number of characters in a buffer exceeds this threshold,
`consult-ripgrep' will be used instead of `consult-line'."
    :type 'integer)

  (defun my-consult-ripgrep-or-line ()
    "Call `consult-line' for small buffers or `consult-ripgrep' for large files."
    (interactive)
    (if (or (not buffer-file-name)
            (buffer-narrowed-p)
            (ignore-errors
              (file-remote-p buffer-file-name))
            (jka-compr-get-compression-info buffer-file-name)
            (<= (buffer-size)
                (/ my-consult-ripgrep-or-line-limit
                   (if (eq major-mode 'org-mode) 2 1))))
        (consult-line)
      (when (file-writable-p buffer-file-name)
        (save-buffer))
      (let ((consult-ripgrep-args
             (concat consult-ripgrep-args
                     " --hidden")))
        (consult-ripgrep (list buffer-file-name)))))

  (defmacro my-consult-with-no-sep (fn)
    (let* ((fn-value (eval fn))
           (old-name (symbol-name fn-value))
           (new-name (concat old-name "-with-no-sep"))
           (doc (documentation fn-value)))
      `(progn (defun ,(intern new-name) ()
                ,doc
                (interactive)
                (require 'orderless)
                (let ((completion-styles '(orderless))
                      (completion-category-defaults nil)
                      (completion-category-overrides nil)
                      (orderless-component-separator 'list))
                  (call-interactively ,fn))
                #',(intern new-name)))))

  ;; from https://github.com/minad/consult/issues/318#issuecomment-882067919
  ;; with some tweaks
  (defun my-consult-line-evil-history (&rest _)
    "Add latest `consult-line' search pattern to the evil search history ring.
This only works with orderless and interprets the whole string as a single
component."
    (when-let ((_ (bound-and-true-p evil-mode))
               (_ (eq evil-search-module 'evil-search))
               (hist (car consult--line-history))
               (orderless-component-separator 'list)
               (pattern (car (orderless-pattern-compiler hist))))
      (evil-push-search-history pattern (eq evil-ex-search-direction 'forward))
      (setq evil-ex-search-pattern (list pattern t t))
      (when evil-ex-search-persistent-highlight
        (evil-ex-search-activate-highlight evil-ex-search-pattern))))

  (my-consult-with-no-sep #'my-consult-ripgrep-or-line)
  (advice-add #'my-consult-ripgrep-or-line :after #'my-consult-line-evil-history)

  (defmacro my-ignore-arg (fn)
    "Define a wrapper for an interactive function that ignores its input.
Unlike `defun',this guarantees to return the defined function symbol."
    (let* ((fn-value (eval fn))
           (old-name (symbol-name fn-value))
           (new-name (concat "my-ignore-arg-" old-name))
           (doc (documentation fn-value)))
      `(progn (defun ,(intern new-name) ()
                ,doc
                (interactive)
                (call-interactively ,fn))
              #',(intern new-name))))
  (defvar-keymap my-global-consult-map)
  (:with-map my-global-consult-map
    (:bind
     ;; "g" (my-with-universal-argument #'consult-ripgrep)
     "f" #'consult-fd
     "b" #'consult-buffer
     "l" #'consult-flymake
     "F" #'consult-locate
     "i" #'consult-imenu
     "o" #'consult-outline
     "m" #'consult-minor-mode-menu
     "x" #'consult-mode-command
     "k" #'consult-man
     "l" #'my-consult-ripgrep-or-line))

  (defmacro my-evil-ex-search- (fn direction)
    (let* ((fn-value (eval fn))
           (dirs (symbol-name (eval direction)))
           (new-name (concat "my-evil-ex-search-" dirs))
           (doc (documentation fn-value)))
      `(progn (defun ,(intern new-name) ()
                ,doc
                (interactive)
                (setq evil-ex-search-direction ,direction)
                (call-interactively ,fn))
              #',(intern new-name))))

  (:global (:rebind #'evil-ex-search-forward (my-evil-ex-search- #'my-consult-ripgrep-or-line-with-no-sep 'forward)
                    #'evil-ex-search-backward (my-evil-ex-search- #'my-consult-ripgrep-or-line-with-no-sep 'backward))))

For consult-grep families and consult-find families, it is possible to convert orderless patterns into their PCRE pattern inputs, as suggested by the Wiki.

(setup consult
  (defun consult--orderless-regexp-compiler (input type &rest _config)
    (setq input (orderless-pattern-compiler input))
    (cons
     (mapcar (lambda (r) (consult--convert-regexp r type)) input)
     (lambda (str) (orderless--highlight input t str))))

  (:option consult--regexp-compiler #'consult--orderless-regexp-compiler))

Embark

embark is probably the most world-changing package in Emacs recently. It basically provides a just-in-time context-aware action list (quite like no-repeating hydra or which-key) in minibuffer on the complete-read candidate or on anything in the editing file.

Reference:

(simple-service
 'home-emacs-embark
 home-profile-service-type
 (list
  (specification->package
   "emacs-embark")))
(setup (:require embark)
  ;; Optionally replace the key help with a completing-read interface
  (:option prefix-help-command #'embark-prefix-help-command)
  (:option embark-cycle-key "C-z")
  (:option (remove embark-indicators)
           'embark-mixed-indicator
           (prepend embark-indicators)
           'embark-minimal-indicator)
  (:with-map minibuffer-local-map (:bind "C-z" #'embark-act))
  (:global (:bind "C-h B" #'embark-bindings) ;; alternative for `describe-bindings'
           (:with-state (normal visual)
             (:bind "g a" #'embark-act
                    "g A" #'my-embark-act-other-window)))
  ;; display embark action buffer at frame bottom
  (:option (prepend display-buffer-alist)
           `(,(rx "*Embark Actions*")
             (display-buffer-in-direction)
             (window . root)
             (direction . below)
             (window-height . fit-window-to-buffer)
             (window-parameters . ((no-other-window . t)
                                   (mode-line-format . none))))))
(setup (:require embark-consult))

I find typing embark-cycle-key both slow (if there are MANY targets) and inconsistent (I need to keep an eye on what is the current target), so I come up with the following advice to make it use consult--read instead.

The way to use it is simply by typing embark-cycle-key as usual, or set the universal argument before doing embark-act. In either case, a consult session will be brought up, and we can select targets by their types in it. Once a target is picked, the embark target list will be rotated until the selected target is at front.

(setup embark-consult
  (defun my-consult-embark--target-candidate (cand)
    (let* ((type (plist-get cand :type))
           (type-string (symbol-name type))
           (target (plist-get cand :target))
           (type (propertize type-string 'consult-embark-target target)))
      (cons type cand)))

  (defun my-consult-embark--target-read (targets)
    (let* ((targets (cl-mapcar #'my-consult-embark--target-candidate targets))
           (indent (+ 2 (apply #'max (cl-mapcar (lambda (target) (length (car target))) targets))))
           (align (propertize " " 'display `(space :align-to (+ left ,indent))))
           (target (consult--read
                    targets
                    :prompt "Target: "
                    :require-match t
                    :category 'embark-target
                    :annotate (lambda (tgt)
                                (let ((target (get-pos-property 0 'consult-embark-target tgt)))
                                  (concat align (embark--truncate-target target))))
                    :lookup #'consult--lookup-cdr)))
      target))

  (:option (prepend completion-category-overrides) '(embark-target (styles orderless-only-initialism)))

  (defun my-embark--rotate-modify-k (args)
    (pcase-let ((`(,targets ,k) args))
      (list targets
            (if-let (((cdr targets)) ;; len >= 2
                     ((plistp (car targets))) ;; is target list
                     ((not (embark--action-repeatable-p this-command))) ;; is not auto rotate after repeat
                     (target (my-consult-embark--target-read targets))
                     (step (cl-position target targets)))
                step
              k))))
  (advice-add #'embark--rotate :filter-args #'my-embark--rotate-modify-k))

TODO: I’m thinking about binding c-u embark-act directly,

(cl-defun my-embark--ignore-target (&key action target &allow-other-keys)
  "If the target is empty (introduced by global), do thing."
  (when (string-empty-p target)
    (embark--ignore-target)))

(defun embark-target-global ()
  (cons 'global ""))
(add-hook 'embark-target-finders #'embark-target-global 100)
(add-to-list 'embark-keymap-alist '(global . my-global-consult-map))
(map-keymap
 (lambda (_key cmd)
   (cl-pushnew 'my-embark--ignore-target
               (alist-get cmd embark-target-injection-hooks)))
 my-global-consult-map)
(defun embark-target-this-buffer ()
  (when-let ((buffer (buffer-name)))
    (cons 'this-buffer buffer)))

(add-hook 'embark-target-finders #'embark-target-this-buffer 98)

(defvar-keymap this-buffer-map
  :doc "Commands to act on current file."
  :parent embark-buffer-map
  "g" #'revert-buffer
  "u" #'vundo)

(add-to-list 'embark-keymap-alist '(this-buffer . this-buffer-map))
(defun embark-target-this-file ()
  (when-let ((file (buffer-file-name)))
    (cons 'this-file file)))

(add-hook 'embark-target-finders #'embark-target-this-file 97)

(defvar-keymap this-file-map
  :doc "Commands to act on current file."
  :parent embark-file-map
  "g" #'revert-buffer)

(add-to-list 'embark-keymap-alist '(this-file . this-file-map))

With embark-live, a buffer is live-updating to show the candidates of the current completing-read, which means vertico’s own view is redundant. Minad Provides the following solution. Note that this needs vertico-multiform-mode.

(setup embark
  (defun +embark-live-vertico ()
    "Shrink Vertico minibuffer when `embark-live' is active."
    (when-let (win (and (string-prefix-p "*Embark Live" (buffer-name))
                        (active-minibuffer-window)))
      (with-selected-window win
        (when (and (bound-and-true-p vertico--input)
                   (fboundp 'vertico-multiform-unobtrusive))
          (vertico-multiform-unobtrusive)))))
  (:with-mode embark-collect-mode
    (:hook +embark-live-vertico)))

I found that very often I want the buffer opened by embark to be somewhere I assign. Adapted from Karthik Chikmagalur’s hack, I now have a way of picking the window (or splitting on-the-fly) by calling my-embark-act-other-window. For minibuffer things are a little bit complicated, and currently I’m using a toggle outside of Embark directly.

(setup embark
  (defvar my-embark--action-ace-window-p nil
    "Whether call `ace-window' before the action.")

  (defvar my-embark--action-ace-window-blacklist (list
                                                  #'embark-done
                                                  #'embark-cycle
                                                  #'embark-toggle-quit
                                                  #'describe-symbol
                                                  #'apropos)
    "Never call `ace-window' for these actions. These are the actions that pop windows
themselves, which should be fixed in other way later.")

  (cl-defun my-embark--ace-window (&rest rest &key run action quit &allow-other-keys)
    "call `ace-window' before the action."
    (when (and my-embark--action-ace-window-p
               quit
               (not (embark--action-repeatable-p action))
               (not (memq action my-embark--action-ace-window-blacklist)))
      (setq my-embark--action-ace-window-p nil)
      (with-demoted-errors "%s"
        (require 'ace-window)
        (let ((aw-dispatch-always t))
          (aw-switch-to-window (aw-select nil)))))
    (apply run :action action :quit quit rest))

  (cl-pushnew 'my-embark--ace-window
              (alist-get :always embark-around-action-hooks))

  (defun my-embark-act-other-window (&optional arg)
    "Perform the following action in ace window."
    (interactive "P")
    (let ((my-embark--action-ace-window-p t))
      (embark-act arg)))

  (defun my-toggle-embark-ace-window ()
    (interactive)
    (when (minibufferp)
      (setq my-embark--action-ace-window-p (not my-embark--action-ace-window-p))
      (message "ace-window-p: [%s]" my-embark--action-ace-window-p)))

  (:with-map vertico-map
    (:bind "C-o" #'my-toggle-embark-ace-window)))

Note: Somehow only post-hooks can recognize (minibufferp).

Tempel

tempel is a “tiny template package for Emacs”, using the built-in template package Tempo’s syntax. I use it instead of famous YASnippet because

  • YASnippet seems unmaintained (update on 2024-02: it seems to be revived!)
  • YASnippet expansion with wrapping (i.e. wrapping region of text into the template) seems weird
  • Tempel uses syntax of built-in Tempo, which is sexp-like expressions.
  • With tempel, multiple templates can be defined within a single file, while YASnippet requires single template per file.
(simple-service
 'home-emacs-tempel
 home-profile-service-type
 (list
  (specification->package "emacs-tempel")
  (specification->package "emacs-eglot-tempel-next")))

The functions here come from tempel’s README.

(setup tempel
  (defun tempel-include (elt)
    (when (eq (car-safe elt) 'i)
      (if-let (template (alist-get (cadr elt) (tempel--templates)))
          (cons 'l template)
        (message "Template %s not found" (cadr elt))
        nil)))
  (with-eval-after-load 'tempel
    (:option (prepend tempel-user-elements) #'tempel-include))
  (:option tempel-path (exdg-config "templates.eld")
           (append my-mode-line-indicators) '(tempel--active "Temp "))
  (defun tempel-setup-capf ()
    (setq-local completion-at-point-functions
                (cons #'tempel-expand completion-at-point-functions)))
  (:with-function tempel-setup-capf
    (:hook-into conf-mode prog-mode text-mode))
  (:with-map tempel-map
    (:bind "M-a" #'tempel-beginning
           "M-e" #'tempel-end
           "M-p" #'tempel-previous
           "M-n" #'tempel-next)))

global templates

fundamental-mode

(date (format-time-string "%Y-%m-%d"))

Eglot-tempel

Tempel itself, unlike YASnippet, does not support LSP snippet expansion out of the box. This feature is notably useful when you auto-complete a function name, in which case the argument list is the snippet.

Anyway, eglot-tempel, as the name suggests, bridges eglot’s snippet interface with tempel. There is also lsp-snippet that might worth checking later.

The version in Guix official channel is as old as 2022, so again I declare a HEAD variation.

(define-public emacs-eglot-tempel-next
  (let ((commit "303c7c24e140121f8bc218249b6169d0471b77b8")
        (last-release-version "0.5.0")
        (revision "0"))
    (package
      (inherit upstream:emacs-eglot-tempel)
      (name "emacs-eglot-tempel-next")
      (version (git-version last-release-version revision commit))
      (source
       (origin
         (method git-fetch)
         (uri (git-reference
               (url "https://github.com/fejfighter/eglot-tempel")
               (commit commit)))
         (file-name (git-file-name name version))
         (sha256
          (base32
           "10scmnkvp2aid9a4bb26cvg8vag6plkrnpg96dylwm0g6rra19zp"))))
      (arguments
       (list #:tests? #false))
      (native-inputs (list)))))
(setup eglot-tempel
  (:hook-into eglot-server-initialized-hook))

Corfu

corfu is a completion-at-point implementation that is much more concise than company.

(simple-service
 'home-emacs-setup
 home-profile-service-type
 (list
  (specification->package
   "emacs-corfu")))
(setup corfu
  (:option corfu-preview-current nil
           corfu-quit-at-boundary nil)
  (:option tab-always-indent 'complete)
  (:with-state (insert emacs)
    (:global (:bind "<CTRL-i>" #'completion-at-point)) ;; see early-init.el
    (:with-map corfu-map (:bind "<escape>" #'corfu-reset
                                "SPC" #'corfu-insert-separator)))
  (defun corfu-enable-always-in-minibuffer ()
    "Enable Corfu in the minibuffer if Vertico/Mct are not active."
    (unless (or (bound-and-true-p mct--active)
                (bound-and-true-p vertico--input))
      (:enable)))
  (add-hook 'minibuffer-setup-hook #'corfu-enable-always-in-minibuffer 1)
  (:require corfu)
  (:with-mode global-corfu-mode (:enable)))

Visual Undo

vundo is basically a less-buggy undo-tree that supports Emacs 28’s new undo-redo.

(simple-service
 'home-emacs-vundo
 home-profile-service-type
 (list
  (specification->package
   "emacs-vundo")))
(setup (:require vundo)
  (:with-map my-leader-map (:bind "u" #'vundo)))

Hideshow

hideshow is Emacs’ built-in code folding package.

(setup hideshow
  (:with-mode hs-minor-mode (:hook-into prog-mode)))

Pulse

pulse is a built-in package that transiently highlights a region (current cursor line, for example). Its callbacks can be added to post jump hooks, so that the jumps are easier to follow.

(setup pulse
  (:with-function pulse-momentary-highlight-one-line
    (:hook-into consult-after-jump-hook imenu-after-jump-hook))
  (defun my-pulse-momentary-highlight-one-line ()
    "Momentary highlight one line if the window buffer changed."
    (when-let* ((old-window (old-selected-window))
                (_ (window-valid-p old-window))
                (old-buffer (with-selected-window old-window (window-buffer)))
                (new-window (selected-window))
                (_ (window-valid-p new-window))
                (new-buffer (with-selected-window new-window (window-buffer)))
                (_ (not (eq old-buffer new-buffer))))
      (pulse-momentary-highlight-one-line)))
  (:with-function my-pulse-momentary-highlight-one-line
    (:hook-into window-state-change-hook)))

electric-pair-mode

electric-pair-mode is a built-in package that auto insert the left bracket/parentheses when we type the left one. It also skip the right bracket/parentheses if we type it. This behavior might be familiar to many IDE users.

(setup elec-pair
  (:with-mode electric-pair-local-mode
    (:hook-into prog-mode minibuffer-setup)))

Aggresive Indent

aggressize-indent-mode basically reindents what you have changed after every change you made.

(simple-service
 'home-emacs-setup
 home-profile-service-type
 (list
  (specification->package
   "emacs-aggressive-indent")))
(setup aggressive-indent
  (:hook-into emacs-lisp-mode scheme-mode)
  (:option aggressive-indent-dont-indent-if '((evil-insert-state-p) (evil-replace-state-p)))
  (defun my-aggressive-indent-after-change ()
    (cond (aggressive-indent-mode
           (add-hook 'evil-normal-state-entry-hook #'aggressive-indent--process-changed-list-and-indent nil t))
          (t
           (remove-hook 'evil-normal-state-entry-hook #'aggressive-indent--process-changed-list-and-indent t))))
  (:hook #'my-aggressive-indent-after-change))

Eshell

I plan on switching to eshell as my main shell. Here are some references:

(setup eshell
  (:option (prepend display-buffer-alist)
           `(,(rx bos "*" (opt (1+ (or alnum "-")) "-") "eshell*")
             display-buffer-in-side-window
             (side . right)
             (slot . 0)
             (window-parameters . ((no-delete-other-windows . t)))
             (window-width . 80))))

fish-completion

fish-completion is a cool package that empowers Eshell with auto-completion feature from the fish shell. This package even has the ability to fallback on auto-completion provided by bash shell, although I’m not using that right now.

(simple-service
 'home-emacs-fish-completion
 home-profile-service-type
 (list
  (specification->package
   "emacs-fish-completion")))
(setup fish-completion
  (:needs "fish")
  (:hook-into eshell-mode))

Magit

magit is an Emacs interface to git, which provides not only commands to call but also a full GUI-like wrapper around git.

(simple-service
 'home-emacs-magit
 home-profile-service-type
 (list
  (specification->package
   "emacs-magit")))
(setup magit
  (:bind "SPC" #'god-execute-with-current-bindings)
  (:with-map (magit-revision-mode-map magit-section-mode-map magit-diff-mode-map) (:bind "SPC" #'god-execute-with-current-bindings))
  (:option magit-display-buffer-function #'display-buffer
           magit-bury-buffer-function #'quit-window ;; play nice with shackle
           evil-collection-magit-use-z-for-folds t
           magit-bind-magit-project-status nil))

Its Evil integration is now a part of evil-collection.

Project

Since Emacs 28, the built-in project.el implements most functionalities needed for project management, which makes projectile unnecessary.

(setup (:require project)
  (:option project-switch-use-entire-map t
           project-list-file (exdg-state "project-list.el"))
  (:with-map my-leader-map (:bind "p" project-prefix-map))
  (:with-map project-prefix-map
    (:bind "m" #'magit-project-status
           "g" (my-ignore-arg #'consult-ripgrep))))
(defun embark-target-project ()
  (cons 'project
        (if-let ((project (project-current nil))
                 (project-name (project-name project)))
            project-name
          "<unknown>")))

(add-hook 'embark-target-finders #'embark-target-project 99)

(add-to-list 'embark-keymap-alist '(project . project-prefix-map))

(map-keymap
 (lambda (_key cmd)
   (cl-pushnew 'embark--ignore-target
               (alist-get cmd embark-target-injection-hooks)))
 project-prefix-map)

Emacsql

emacsql is “a high-level Emacs Lisp RDBMS front-end”, which provides a consistent facade for different sqlite integration implementations. There is one tagged version in Guix package upstream, but it is too old for my need (and it comes with too many unnecessary dependencies), see below.

(define-public emacs-emacsql-minimal
  (package
    (inherit upstream:emacs-emacsql)
    (name "emacs-emacsql-minimal")
    (inputs (list upstream:emacs-minimal))
    (propagated-inputs (list))
    (build-system emacs-build-system)
    (arguments
     '(#:include '("emacsql.el" "emacsql-compiler.el" "emacsql-sqlite-common.el")))))

emacsql-sqlite-builtin, on the other hand, is the built-in integration shipped with Emacs 29. We have to use Emacs 29 to compile it, instead of emacs-minimal, to makes the build phase happy.

(define-public emacs-emacsql-sqlite-builtin
  (package
    (inherit emacs-emacsql-minimal)
    (name "emacs-emacsql-sqlite-builtin")
    (propagated-inputs (list emacs-emacsql-minimal))
    (build-system emacs-build-system)
    (arguments
     `(#:include '("emacsql-sqlite-builtin.el")))))
(simple-service
 'home-emacs-emacsql
 home-profile-service-type
 (list
  (specification->package
   "emacs-emacsql-sqlite-builtin")))

Epub

Emacs’ built-in doc-view-mode is said to support Epub format, but I’ve never got it to work. nov.el to the rescue.

(define-public emacs-nov-el-next
  (let ((commit "cc31ce0356226c3a2128119b08de6107e38fdd17")
        (last-release-version "0.4.0")
        (revision "0"))
    (package
     (inherit upstream:emacs-nov-el)
     (name "emacs-nov-el-next")
     (version (git-version last-release-version revision commit))
     (source
      (origin
       (method git-fetch)
       (uri (git-reference
             (url "https://depp.brause.cc/nov.el.git")
             (commit commit)))
       (file-name (git-file-name name version))
       (sha256
        (base32
         "0k09dd0j8m8607dv61qm4q1jk9hvn39sxzk5ckcalafjanp7l0r6")))))))
(simple-service
 'home-emacs-nov-el
 home-profile-service-type
 (list
  (specification->package
   "emacs-nov-el-next")))
(setup nov
  (:option nov-save-place-file (exdg-state "nov-save-place.el")
           (prepend auto-mode-alist) `(,(rx ".epub" eos) . nov-mode)))

Org Mode

From its website

Org mode is for keeping notes, maintaining TODO lists, planning projects, and authoring documents with a fast and effective plain-text system.

this is only a facial overall summary of what org-mode is usually used for. It is so powerful that It is one of the reasons I switched from Neovim to Emacs.

Useful References:

  • org-almanac, an “awesome”-ish list of what people are using Org Mode for.
(simple-service
 'home-emacs-org
 home-profile-service-type
 (specifications->packages
  (list "emacs-org"
        "emacs-evil-org"
        "emacs-toc-org"
        "emacs-org-appear-next"
        "emacs-org-download-next")))
(setup org
  <<org-setup>>
  (:hook visual-line-mode variable-pitch-mode))

General Settings

Turn on org-indent, aka clean view by default:

(:option org-startup-indented t)

Enforce to-do dependencies (i.e. children block their parent)

(:option org-enforce-todo-dependencies t)

When the cursor is on the headline, c-a c-e will stop after the leading stars and before the tags, respectively. Likewise, c-k will only delete up to the tags. Moreover, evil-org respects these settings.

(:option org-special-ctrl-a/e t)
(:option org-special-ctrl-k t)

Prevent M-RET from splitting the line if the line is a headline or an item.

(:option org-M-RET-may-split-line '((default . nil)))

Update #+last_modified every time an org file is saved.

(defun my-org-autoupdate-timestamp ()
  (setq-local time-stamp-active t
              time-stamp-start "#\\+last_modified:[ \t]*"
              time-stamp-end "$"
              time-stamp-format "\[%Y-%02m-%02d %3a %02H:%02M\]")
  (add-hook 'before-save-hook #'time-stamp nil t))
(:hook my-org-autoupdate-timestamp)
(:option org-persist-directory (exdg-cache "org-persist"))

Org-mode defaults to “show everything” whenever a buffer is initially opened, which includes property drawers. I think the default was “show all”, which unfold most things except properties, but the default was changed upstream at some point. But anyway, “show all” is the desired behavior for me, because using org-roam means there are properties that I have no interest in everywhere.

(:option org-startup-folded 'showall)

Templates:

org-mode

(begin "#+begin_" (s name) n> r> n "#+end_" name)
(elisp "#+begin_src emacs-lisp" n> r> n "#+end_src" :post (org-edit-src-code))
(scheme "#+begin_src scheme" n> r> n "#+end_src" :post (org-edit-src-code))
(id :post (org-roam-node-insert))

Task Management

I generally follow the GTD way as my task management system.

Tasks and Logs

Todo state keywords. The todo state is simple:

(:option org-todo-keywords
         '((sequence "TODO(t!)" "NEXT(e!)" "WAIT(w@/@)" "|" "DONE(d@)")
           ("|" "CANCELED(c@)")
           ("|" "MEETING(m)")
           ("|" "PHONE(p)")))

Log into a LOGBOOK drawer so that things are folded when we want to read about outcome descriptions

(:option org-log-into-drawer t)

When refiling, log down a timestamp:

(:option org-log-refile t)

I found that usually I have something to say when I closing a task, for example a link to the reproduction note. Thus I’d like to have closing note by default.

(:option org-log-done 'note)

Put newer note at the top:

(:option org-reverse-note-order t)
Effort Measurement and Time Cost Estimates

Org mode provides the feature to estimate effort and track time spent on a task.

First, if something somehow has a 0:00 duration, don’t count it.

(:option org-clock-out-remove-zero-time-clocks t)

Clock out when a task is DONE or CANNCELED

(:option org-clock-out-when-done t)

Sometimes, I forget to clock out before rebooting or shutting down. Org Clock provides the feature to continue the previous unfinished task when Emacs restarts, which can be handy in this case.

(:option org-clock-persist t
         org-clock-persist-file (exdg-state "org-clock-persist.el"))
(with-eval-after-load 'org
  (org-clock-persistence-insinuate))

Literate Programming

References:

Evil Org

evil-org is org mode’s evil integration. It provides not simply keybindings, but also text objects.

(setup (:require evil-org)
  (:also-load org evil-org-agenda)
  (:hook-into org-mode)
  (evil-org-set-key-theme)
  (evil-org-agenda-set-keys)
  (:with-map org-mode-map
    (:with-state motion (:bind "RET" #'org-open-at-point))))

Toc Org

toc-org will automatically update the content of the first heading with a :TOC: tag in an org file to show an up-to-date TOC whenever the file is saved. Handy!

(setup toc-org
  (:also-load org)
  (:hook-into org-mode))

Org Appear

org-appear is a minor mode that “toggle visibility of hidden Org mode element parts upon entering and leaving an element”, and it works with Evil very well.

Its package definition in the Guix Official Channel is for the “latest” release version, which is as old as January 2022. So I makes a variation to use the master branch HEAD at the time of writing.

(define-public emacs-org-appear-next
  (let ((commit "81eba5d7a5b74cdb1bad091d85667e836f16b997")
        (last-release-version "0.3.0")
        (revision "0"))
    (package
     (inherit upstream:emacs-org-appear)
     (name "emacs-org-appear-next")
     (version (git-version last-release-version revision commit))
     (source
      (origin
       (method git-fetch)
       (uri (git-reference
             (url "https://github.com/awth13/org-appear")
             (commit commit)))
       (file-name (git-file-name name version))
       (sha256
        (base32
         "1jh2rdp7rx1hnsfky5di1amz8rc5jf0qlc5ykr09m5f9fpz9m9x6")))))))
(setup org-appear
  (:option org-appear-trigger 'manual
           org-appear-autolinks t
           org-appear-autosubmarkers t
           org-appear-autoentities t)
  (defun my-org-appear-setup ()
    (cond (org-appear-mode
           (add-hook 'evil-normal-state-exit-hook #'org-appear-manual-start nil t)
           (add-hook 'evil-normal-state-entry-hook #'org-appear-manual-stop nil t))
          (t
           (remove-hook 'evil-normal-state-exit-hook #'org-appear-manual-start t)
           (remove-hook 'evil-normal-state-entry-hook #'org-appear-manual-stop t))))
  (:hook-into org-mode)
  (:hook #'my-org-appear-setup))

Personal Knowledge Management

I believe strongly that PIM as its adjective “personal” implies, is something that varies from individuals to individuals. That is, there is no such “universal best practice” for everyone. Thus, what we really need is a highly customizable framework to build our own variation. Luckily, org mode fits into this ground.

I use a personal-hacked variation of Zettelkasten.

Org Id

Enable tracking org heading links using globally unique UIDs. This is a must-have even without org-roam, because org mode won’t fix the broken links when you refile/archive some subtrees to a different file.

(setup org-id
  (:option org-id-track-globally t
           org-id-link-to-org-use-id 'create-if-interactive
           org-id-ts-format "%Y%m%dT%H%M%SM%3N"
           org-id-locations-file (exdg-state "org-id-locations.el")))
Custom Links

lfile looks up the link by querying plocate database, which is a pre-indexed DB for local files. Based on blog post from Karl Voit.

(setup ol
  (defun my-handle-lfile-link (opener querystring)
    ;; get a list of hits
    (let ((queryresults (split-string
                         (s-trim
                          (shell-command-to-string
                           (concat
                            "plocate --existing "
                            querystring
                            " "
                            )))
                         "\n" t)))
      ;; check length of list (number of lines)
      (cond
       ((= 0 (length queryresults))
        ;; edge case: empty query result
        (message "Sorry, no results found for query: %s" querystring))
       ((= 1 (length queryresults))
        ;; exactly one hit:
        (funcall opener (car queryresults))
        )
       (t
        ;; in any other case:
        (alert (format "Sorry, multiple results found for query: %s" querystring))
        ;; FIXXME: ask user to select among multiple hits.
        )
       )))
  (org-link-set-parameters
   "lfile"
   :follow (lambda (filename) (my-handle-lfile-link #'consult-file-externally filename))
   :help-echo "Opens the file located via \"locate\" with your default application"
   ))

Here is an attempt to implement Link Tags.

(setup ol
  (defvar my-org-id-link-special-defs '(("related" :follow org-id-open :face 'org-tag
                                         :help-echo "Related to the given topic.")
                                        ("follow" :follow org-id-open :face 'org-tag
                                         :help-echo "Is a Follow-up of the given note.")
                                        ("under" :follow org-id-open :face 'org-tag
                                         :help-echo "Is a sub-topic given note.")
                                        ("translate" :follow org-id-open :face 'org-tag
                                         :help-echo "Is a translation of the given note.")))
  (dolist (def my-org-id-link-special-defs)
    (apply #'org-link-set-parameters def))
  (defun my-org-id-link--ctor- (type desc)
    (propertize type 'desc desc))
  (defun my-org-id-link--ctor (def)
    (let* ((type (car def))
           (rest (cdr def))
           (desc (plist-get rest :help-echo)))
      (my-org-id-link--ctor- type desc)))
  (defvar my-org-id-link--types `(,(my-org-id-link--ctor- "id" "Normal org-roam link.")
                                  ,@(cl-mapcar #'my-org-id-link--ctor my-org-id-link-special-defs)))
  (defun my-org-id-link-type-read (&optional prompt)
    (let ((align (propertize " " 'display '(space :align-to (+ left 20)))))
      (consult--read
       my-org-id-link--types
       :prompt (or prompt "Types: ")
       :annotate (lambda (target) (concat align (get-pos-property 0 'desc target)))
       :require-match t)))
  (defun my-org-link-modify-type ()
    "Modify the type of the org id link at point."
    (interactive)
    (when-let (((org-in-regexp org-link-any-re))
               (remove (list (match-beginning 0) (match-end 0)))
               (target (or (match-string-no-properties 2)
                           (match-string-no-properties 0)))
               (desc (match-string-no-properties 3))
               (type-regex (rx bol (group (+ alnum)) ":"))
               ((string-match type-regex target))
               (old-type (match-string-no-properties 1 target))
               (old-type-fancy (propertize old-type 'face 'org-tag))
               (prompt (format "Modify from %s: " old-type-fancy))
               (type-rep (my-org-id-link-type-read prompt))
               (target (concat type-rep (string-remove-prefix old-type target))))
      (apply #'delete-region remove)
      (org-insert-link nil target desc)))
  (:with-map embark-org-link-map
    (:bind "m" #'my-org-link-modify-type))
  (:option (prepend embark-target-injection-hooks) '(my-org-link-modify-type embark--ignore-target)))
Org Roam

org-roam basically does 2 things:

  1. Use a sqlite database to cache everything that is getting slow as notes scaling up
  2. Using this database to display “backlinks” for a note, a fancy word standing for the links that point to the current note.

This means that, giving that org-roam is quite stable now, we can use the database to do many crazy things!

Again the packaged version in Guix official packages is quite old, so here is the git HEAD version. I also clear up the propagated-inputs list a little bit, especially by adding a simple hack to remove the redundant dependency on the old emacs-sqlite. Similar to emacsql-sqlite-built-in, it requires Emacs 29 to compile.

(define-public emacs-org-roam-next
  (let ((commit "8667e441876cd2583fbf7282a65796ea149f0e5f")
        (last-release-version "2.2.2")
        (revision "0"))
    (package
      (inherit upstream:emacs-org-roam)
      (name "emacs-org-roam-next")
      (version (git-version last-release-version revision commit))
      (source
       (origin
         (method git-fetch)
         (uri (git-reference
               (url "https://github.com/org-roam/org-roam")
               (commit commit)))
         (file-name (git-file-name name version))
         (sha256
          (base32
           "1j0xg09nzhm35pas622yhmxfx7fw9kch1kyrabg072j6hl83ahsw"))))
      (propagated-inputs
       (list upstream:emacs-dash
             upstream:emacs-magit
             upstream:emacs-org
             emacs-emacsql-sqlite-builtin))
      (arguments
       (append
        (substitute-keyword-arguments (package-arguments upstream:emacs-org-roam)
          ((#:phases phases)
           `(modify-phases ,phases
              (add-after 'patch-exec-paths 'drop-emacsql-sqlite-dependency
                (lambda _
                  (substitute* "org-roam.el"
                    (("\\(require 'emacsql-sqlite\\)") ""))
                  #t))))))))))

And a missing gem org-roam-ql, which has a query syntax and feature set similar to org-ql (It starts as a “spin-off” from org-ql I think, see this and this). Basically it turns a s-exp query into a series of SQL queries to the org-roam database.

(define-public emacs-org-roam-ql
  (let ((commit "f628fef081394f159f196f4350132aecb3edb8cc")
        (last-release-version "0.2")
        (revision "1")
        (url "https://github.com/ahmed-shariff/org-roam-ql"))
    (package
     (name "emacs-org-roam-ql")
     (version (git-version last-release-version revision commit))
     (source
      (origin
       (method git-fetch)
       (uri (git-reference
             (url url)
             (commit commit)))
       (file-name (git-file-name name version))
       (sha256
        (base32
         "1ssxvy6y79f035whk9b8jg1vqsy6vymgq9yrzbxv06g5vsggvlh5"))))
     (build-system emacs-build-system)
     (propagated-inputs
      (list upstream:emacs-magit
            upstream:emacs-org-super-agenda
            upstream:emacs-s
            upstream:emacs-transient
            emacs-org-roam-next))
     (arguments
      `(#:include '("^org-roam-ql.el")
        #:tests? #false))
     (home-page url)
     (synopsis "Query language for org-roam")
     (description "This package provides an interface to easily query and display
results from your org-roam database.")
     (license license:gpl3+))))
(simple-service
 'home-emacs-org-roam
 home-profile-service-type
 (specifications->packages
  (list
   "emacs-org-roam-next"
   "emacs-org-roam-ql")))
(setup org-roam
  (setq org-roam-db-gc-threshold (* 256 1024 1024)) ;; the type check is buggy
  (:option org-roam-database-connector 'sqlite-builtin
           org-roam-db-location (exdg-state "org-roam.db")
           org-roam-db-update-on-save nil
           org-roam-protocol-store-links nil
           org-roam-link-auto-replace nil ;; no longer needed; cause hang
           org-roam-directory (expand-file-name "notes" (xdg-user-dir "DOCUMENTS"))
           org-roam-node-display-template (concat "${hierarchy:*} " (propertize "${tags:10}" 'face 'org-tag))
           org-roam-capture-templates '(("d" "default" plain "%?"
                                         :target (file+head  "%(format-time-string org-id-ts-format).org"
                                                             "#+title: %(titlecase--string \"${title}\" titlecase-style)\n#+date: %U\n#+last_modified: %U\n")
                                         :unnarrowed t)))
  ;; from https://github.com/org-roam/org-roam/issues/1565
  (with-eval-after-load 'org-roam-node
    (cl-defmethod org-roam-node-hierarchy ((node org-roam-node))
      "Return the hierarchy for the node."
      (let ((title (org-roam-node-title node))
            (olp (org-roam-node-olp node))
            (level (org-roam-node-level node))
            (filetitle (org-roam-node-file-title node)))
        (concat
         (when (> level 0) (concat filetitle " > "))
         (when (> level 1) (concat (string-join olp " > ") " > "))
         title))))
  (:with-mode org-roam-db-autosync-mode (:enable))
  (:with-function org-roam-db-sync (:hook-into midnight-hook))
  (defun my-org-roam--node-file-p (node)
    "Return if node is top-level."
    (= (org-roam-node-level node) 0))

  (:with-map my-global-consult-map
    (:bind "n" #'my-org-roam-node-find))
  (:option (prepend embark-target-injection-hooks)
           '(my-org-roam-node-find my-embark--ignore-target)))

(defun my-org-roam-node-this-file (&optional assert)
  (save-excursion
    (goto-char (point-min))
    (org-roam-node-at-point assert)))

(defun my-org-roam-buffer-display-dedicated (node)
  "Launch NODE dedicated Org-roam buffer.
Unlike the persistent `org-roam-buffer', the contents of this
buffer won't be automatically changed and will be held in place.

In interactive calls prompt to select NODE, unless called with
`universal-argument', in which case NODE will be set to
`my-org-roam-node-this-file'."
  (interactive
   (list (if current-prefix-arg
             (my-org-roam-node-this-file 'assert)
           (org-roam-node-read nil #'my-org-roam--node-file-p nil 'require-match))))
  (org-roam-buffer-display-dedicated node))

(defun my-org-roam-buffer-persistent-redisplay ()
  "Recompute contents of the persistent `org-roam-buffer'.
Has no effect when there's no `my-org-roam-node-this-file'."
  (when-let ((node (my-org-roam-node-this-file)))
    (unless (equal node org-roam-buffer-current-node)
      (setq org-roam-buffer-current-node node
            org-roam-buffer-current-directory org-roam-directory)
      (with-current-buffer (get-buffer-create org-roam-buffer)
        (org-roam-buffer-render-contents)
        (add-hook 'kill-buffer-hook #'org-roam-buffer--persistent-cleanup-h nil t)))))

(advice-add #'org-roam-buffer-persistent-redisplay :override #'my-org-roam-buffer-persistent-redisplay)

(cl-defun my-org-roam-backlinks-get (node &key type)
  "Return the backlinks for NODE.

 When UNIQUE is nil, show all positions where references are found.
 When UNIQUE is t, limit to unique sources."
  (let* ((sql [:select [links:source links:dest links:pos links:properties]
                       :from links
                       :inner-join nodes
                       :on (= links:dest nodes:id)
                       :where (= nodes:file $s1)
                       :and (= links:type $s2)])
         (backlinks (org-roam-db-query sql (org-roam-node-file node) type)))
    (cl-loop for backlink in backlinks
             collect (pcase-let ((`(,source-id ,dest-id ,pos ,properties) backlink))
                       (org-roam-populate
                        (org-roam-backlink-create
                         :source-node (org-roam-node-create :id source-id)
                         :target-node (org-roam-node-create :id dest-id)
                         :point pos
                         :properties properties))))))

(cl-defun my-org-roam-backlinks-section (node &key heading type (show-backlink-p nil))
  "The backlinks section for NODE.

When UNIQUE is nil, show all positions where references are found.
When UNIQUE is t, limit to unique sources.

When SHOW-BACKLINK-P is not null, only show backlinks for which
this predicate is not nil."
  (when-let ((backlinks (seq-sort #'org-roam-backlinks-sort (my-org-roam-backlinks-get node :type type))))
    (magit-insert-section ((,intern (concat "org-roam-backlinks-" type)))
      (magit-insert-heading heading)
      (dolist (backlink backlinks)
        (when (or (null show-backlink-p)
                  (and (not (null show-backlink-p))
                       (funcall show-backlink-p backlink)))
          (org-roam-node-insert-section
           :source-node (org-roam-backlink-source-node backlink)
           :point (org-roam-backlink-point backlink)
           :properties (org-roam-backlink-properties backlink))))
      (insert ?\n))))

(setopt org-roam-mode-sections
        '((my-org-roam-backlinks-section :type "translate" :heading "Translated to:")
          (my-org-roam-backlinks-section :type "under" :heading "Super-topic Of:")
          (my-org-roam-backlinks-section :type "follow" :heading "Followed By:")
          (my-org-roam-backlinks-section :type "id" :heading "Backlinks:")
          org-roam-reflinks-section
          (my-org-roam-backlinks-section :type "related" :heading "Related:")))


;; org roam buffer placement
(setup org-roam
  (:unbind "SPC")
  (:option (prepend display-buffer-alist)
           '((derived-mode . org-roam-mode)
             (display-buffer-reuse-mode-window display-buffer-in-side-window)
             (mode . org-roam-mode) ;; unless specified it is checked with eq instead of derived-p
             (side . right)
             (slot . 0)
             (window-parameters . ((no-delete-other-windows . t)))
             (window-width . 80))))

Add consult source:

(setup org-roam
  (defvar consult--source-org-roam
    (list :name     "Notes"
          :category 'org-roam-buffer
          :narrow   ?o
          :face     'consult-buffer
          :history  'buffer-name-history
          :state    #'consult--buffer-state
          :annotate
          (lambda (buffer)
            (with-current-buffer buffer
              (org-roam-node-file-title
               (my-org-roam-node-this-file 'assert))))
          :items
          (lambda ()
            (consult--buffer-query :mode 'org-mode
                                   :predicate #'org-roam-buffer-p
                                   :as #'consult--buffer-pair))))
  (with-eval-after-load 'consult
    (:option (append consult-buffer-sources) consult--source-org-roam)))
(setup org-roam-ql
  (defun my-org-roam-ql--expansion-ft (title &optional exact)
    "Expansion function that query TITLE at top level.
ft stands for file-title."
    `(and (title ,title ,exact) (level 0)))
  (cl-defun my-org-roam-ql--expand-related (&rest tags &key (combine :and) &allow-other-keys)
    "Expansion function for related backlinks.
Example: (related ``Algo'' ``Hardware'')"
    `(backlink-to (or ,@(cl-mapcar (lambda (tag) `(ft ,tag t)) tags)) :type "related" :combine ,combine))
  (:when-loaded
    (org-roam-ql-defexpansion 'ft "Compare to `title' of a file node" #'my-org-roam-ql--expansion-ft)
    (org-roam-ql-defexpansion 'related "Related" #'my-org-roam-ql--expand-related)))

Side notes: org-roam has some issue with org-element--cache-sync that cause org-mode to hang on saving occasionally. I’m still trying to figure out why.

My org-roam-node-find implementation that is able to show my link tags in annotation. This with the recent orderless updates allows me to filter notes by tags. my-org-roam--node-to-tags-table’s implementation technically should be easier and faster, but somehow GROUP_CONCAT does not work correctly with Emacsql.

(setup org-roam
  (:require org-roam-ql)
  (defun my-org-roam--name-table ()
    "Return a table of id to name."
    (let* ((nodes (org-roam-ql-nodes '(level 0)))
           (table (make-hash-table
                   :test #'equal
                   :size (length nodes))))
      (cl-loop for node in nodes do
               (puthash (org-roam-node-id node) (org-roam-node-title node) table))
      table))

  (defun my-org-roam--node-to-tags-table (name-table type prefix)
    "Return an table of id to its forward links (as list of names). PREFIX is
put before each name. TYPE is the type of the links."
    (let* ((s-ds (org-roam-db-query '[:select [source dest] :from links :where (= type $s1)] type))
           (table (make-hash-table
                   :test #'equal)))
      (cl-loop for s-d in s-ds do
               (puthash (car s-d)
                        (cons
                         (concat prefix (gethash (cadr s-d) name-table))
                         (gethash (car s-d) table nil))
                        table))
      table))

  (defun my-org-roam-node-find ()
    "Find top-level nodes."
    (interactive)
    (let* ((name-table (my-org-roam--name-table))
           (related-table (my-org-roam--node-to-tags-table name-table "related" "#"))
           (under-table (my-org-roam--node-to-tags-table name-table "under" "@"))
           (nodes (let ((nodes-temp nil))
                    (maphash (lambda (id name) (push
                                                (cons (propertize (string-truncate-left name 140) 'node-id id) id)
                                                nodes-temp))
                             name-table)
                    nodes-temp))
           (indent (apply #'max (cl-mapcar (lambda (node) (length (car node))) nodes)))
           (align (propertize " " 'display `(space :align-to (+ left ,indent))))
           (annotate (lambda (node)
                       (let* ((id (get-pos-property 0 'node-id node))
                              (related (string-join (gethash id related-table)))
                              (under (string-join (gethash id under-table))))
                         (concat align under related))))
           (found (consult--read
                   nodes
                   :prompt "Org-Roam: "
                   :require-match nil
                   :category 'org-roam-node
                   :annotate annotate
                   :lookup (lambda (selected &rest rest)
                             (if-let (found (apply #'consult--lookup-cdr selected rest))
                                 (cons 'found found)
                               (cons 'new selected))))))
      (if (eq 'found (car found))
          (org-roam-id-open (cdr found) nil)
        (org-roam-capture-
         :node (org-roam-node-create :title (cdr found))
         :templates nil
         :props '(:finalize find-file))))))
Bibliography
(defvar my-global-bibliography
  (list (expand-file-name "notes/refs.bib" (xdg-user-dir "DOCUMENTS")))
  "A list to global bib files.")
ebib

Ebib is technically not related to org-mode in most aspects. It is a front-end to BibTeX/BibLaTeX files.

(simple-service
 'home-emacs-ebib
 home-profile-service-type
 (specifications->packages
  (list
   "emacs-ebib")))
(setup ebib
  (:option ebib-bibtex-dialect 'biblatex
           ebib-preload-bib-files my-global-bibliography
           ebib-use-timestamp t
           ebib-file-search-dirs (list (expand-file-name "resources" (xdg-user-dir "DOCUMENTS")))
           ;; (remove ebib-hidden-fields) "isbn"
           ebib-layout 'index-only))

(defun my--ebib-overwrite-current-entry-field-value (field value)
  (when value
    (ebib-set-field-value field value
                          (ebib--get-key-at-point)
                          ebib--cur-db 'overwrite nil)
    (ebib--set-modified t ebib--cur-db)
    (ebib--update-entry-buffer-keep-note)))

(defun my-fetch-ebook-metadata-by-isbn (isbn)
  "This requires calibre's `fetch-ebook-metadata' in path to work."
  (interactive "s")
  (require 'dom)
  (let* ((opf (with-temp-buffer
                (call-process "fetch-ebook-metadata" nil '(t nil) nil "-i" isbn "-o")
                (delete-matching-lines (rx "Using proxies:" whitespace) (point-min) (point-max))
                (libxml-parse-xml-region (point-min) (point-max))))
         (title (dom-text (dom-by-tag opf 'title)))
         (author (dom-text (dom-by-tag opf 'creator)))
         (date (dom-text (dom-by-tag opf 'date)))
         (publisher (dom-text (dom-by-tag opf 'publisher)))
         (isbn (dom-text
                (cl-find-if
                 (lambda (node) (string= (dom-attr node 'scheme) "ISBN"))
                 (dom-by-tag opf 'identifier)))))
    (my--ebib-overwrite-current-entry-field-value "title" title)
    (my--ebib-overwrite-current-entry-field-value "date" date)
    (my--ebib-overwrite-current-entry-field-value "author" author)
    (my--ebib-overwrite-current-entry-field-value "publisher" publisher)
    (my--ebib-overwrite-current-entry-field-value "isbn" isbn)))
citar

There are quite a lot bibliographic packages, among which I use citar.

(simple-service
 'home-emacs-citar
 home-profile-service-type
 (specifications->packages
  (list
   "emacs-citar-next"
   "emacs-citar-org-roam-next")))
(define-public emacs-citar-next
  (let ((commit "885b86f6733fd70f42c32dd7791d3447f93db990")
        (last-release-version "1.4.0")
        (revision "0"))
    (package
      (inherit upstream:emacs-citar)
      (name "emacs-citar-next")
      (version (git-version last-release-version revision commit))
      (source
       (origin
         (method git-fetch)
         (uri (git-reference
               (url "https://github.com/emacs-citar/citar")
               (commit commit)))
         (file-name (git-file-name name version))
         (sha256
          (base32
           "1kzwllhcn77z6gsdxl6r1csv9nj64qbgznpy8r8kvnri3fl55w4h")))))))
(setup citar
  (:option
   org-cite-global-bibliography my-global-bibliography
   org-cite-insert-processor 'citar
   org-cite-follow-processor 'citar
   org-cite-activate-processor 'citar
   citar-bibliography my-global-bibliography))

It comes with embark integration, where citar-embark-mode is a global mode that introduce the target and actions, and citar-at-point-function (the callback called by org-cite in org-open-at-point) can also be set to use embark. As I understand it, it is meaningless to set citar-at-point-function this way without turning on citar-embark-mode, since all embark can do is to provides actions to recognized targets.

(setup citar-embark
  (:option citar-at-point-function #'embark-act)
  (:enable))

citar comes with its own org-roam integration as a separate package:

(define-public emacs-citar-org-roam-next
  (package
    (inherit upstream:emacs-citar-org-roam)
    (name "emacs-citar-org-roam-next")
    (propagated-inputs (list emacs-org-roam-next emacs-citar-next))))

As far as I can tell, citar-org-roam-mode automatically sets up citar-notes-sources, making the related configuration unnecessary.

(setup citar-org-roam
  (:option citar-org-roam-note-title-template "${author editor}: ${title}")
  (defun my-citar-org-roam--create-capture-note (citekey entry)
    "Open or create org-roam node for CITEKEY and ENTRY."
    ;; adapted from https://jethrokuan.github.io/org-roam-guide/#orgc48eb0d
    (let ((title (citar-format--entry
                  citar-org-roam-note-title-template entry)))
      (org-roam-capture-
       :templates
       '(("r" "reference" plain "%?" :if-new
          (file+head
           "%(format-time-string org-id-ts-format).org"
           "#+title: ${title}\n#+date: %U\n#+last_modified: %U\n")
          :immediate-finish t
          :unnarrowed t))
       :info (list :citekey citekey)
       :node (org-roam-node-create :title title)
       :props '(:finalize find-file))
      (org-roam-ref-add (concat "@" citekey))))
  (:enable)
  (advice-add #'citar-org-roam--create-capture-note :override #'my-citar-org-roam--create-capture-note))

TODO: What’s the point of citar-library-paths?

One great feature of citar-org-roam is that we can have multiple notes per reference key. This makes it possible to split very long literature notes for textbooks into separate files (or just headings), per chapter for example.

Org Download

org-download, despite its name, is an all-in-one image insertion solution for org-mode. It saves the image, no matter where it is from, online or in clipboard, and then inserts the link into org-mode.

(define-public emacs-org-download-next
  (let ((commit "19e166f0a8c539b4144cfbc614309d47a9b2a9b7")
        (last-release-version "0.1.0")
        (revision "0"))
    (package
     (inherit upstream:emacs-org-download)
     (name "emacs-org-download-next")
     (version (git-version last-release-version revision commit))
     (source
      (origin
       (method git-fetch)
       (uri (git-reference
             (url "https://github.com/abo-abo/org-download")
             (commit commit)))
       (file-name (git-file-name name version))
       (sha256
        (base32
         "0a2nw2vf9j335yz40x10q0vmnhxkn9frrm82apvjqsl5p7igvzvs")))))))

I mainly use it to keep images referenced in org-roam notes. There is no other place where I need referencing images anyway!

(setup org-download
  (:option org-download-backend "wget \"%s\" -O \"%s\""
           org-download-image-dir (expand-file-name "images" org-roam-directory)
           org-download-method 'directory
           org-download-heading-lvl nil
           org-download-screenshot-method "maim -s %s"))

Style and Faces

This part of code is basically grabbed from Beautifying Org Mode in Emacs by zzamboni.

Hide ===, ~ and other emphasis markers, and fontify src block natively:

(:option org-hide-emphasis-markers t
         org-use-sub-superscripts '{}
         org-src-fontify-natively t
         org-tags-column 0)

Detached

detached utilizes dtach and provides Emacs-integrated features. It can do many cool things, see EmacsConf2022 Talk for its demonstration.

(simple-service
 'home-emacs-detached
 home-profile-service-type
 (list
  (specification->package
   "emacs-detached")))
(setup detached
  (:option detached-session-directory (exdg-cache "detached-sessions/")
           detached-db-directory (exdg-state "detached-db/"))
  (detached-init))

Its consult integration is currently broken on my site, so let’s fix it:

(setup detached-consult
  (cl-defun my-detached-consult--source-items (&key (seq #'seq-filter) pred)
    (mapcar #'car
            (funcall seq (lambda (s) (funcall pred (cdr s)))
                     (detached-session-candidates (detached-get-sessions)))))


  (:when-loaded
    (consult-customize
     detached-consult--source-active-session
     :items
     (lambda ()
       (my-detached-consult--source-items :pred #'detached-session-active-p)))

    (consult-customize
     detached-consult--source-inactive-session
     :items
     (lambda ()
       (my-detached-consult--source-items :pred #'detached-session-inactive-p)))

    (consult-customize
     detached-consult--source-failure-session
     :items
     (lambda ()
       (my-detached-consult--source-items :pred #'detached-session-failed-p)))

    (consult-customize
     detached-consult--source-success-session
     :items
     (lambda ()
       (my-detached-consult--source-items :seq #'seq-remove :pred #'detached-session-failed-p)))

    (consult-customize
     detached-consult--source-local-session
     :items
     (lambda ()
       (my-detached-consult--source-items :pred #'detached-session-localhost-p)))

    (consult-customize
     detached-consult--source-remote-session
     :items
     (lambda ()
       (my-detached-consult--source-items :pred #'detached-session-remotehost-p)))

    (consult-customize
     detached-consult--source-current-session
     :items
     (lambda ()
       (let ((host-name (car (detached--host))))
         (my-detached-consult--source-items :pred (lambda (x)
                                                    (string= (detached-session-host-name x) host-name))))))))

English

Linting

(simple-service
 'home-emacs-flymake-vale
 home-profile-service-type
 (list
  (specification->package
   "emacs-flymake-vale")))
(define-public emacs-flymake-vale
  (let ((commit "914f30177dec0310d1ecab1fb798f2b70a018f24")
        (last-release-version "0.0.1")
        (revision "0")
        (url "https://github.com/tpeacock19/flymake-vale"))
    (package
     (name "emacs-flymake-vale")
     (version (git-version last-release-version revision commit))
     (source
      (origin
       (method git-fetch)
       (uri (git-reference
             (url (string-append url ".git"))
             (commit commit)))
       (file-name (git-file-name name version))
       (sha256
        (base32
         "1fi5z1fq9lq0z74v6w70pflh2d9wjfzl5km5jpsgv065y4b3rj3j"))))
     (build-system emacs-build-system)
     (home-page url)
     (synopsis "Flymake support for Vale")
     (description "Vale is a natural language linter.
So with flymake-vale you get on-the-fly natural language linting.")
     (license license:gpl3+)
     (propagated-inputs (list upstream:go-github-com-errata-ai-vale)))))
(setup flymake-vale
  (:with-function flymake-vale-load
    (:hook-into text-mode)))

Capitalizing

titlecase solves one of the hardest problem in (English) writing: capitalizing titles. its most impressing feature is that it supports many standard styles, like Chicago and APA. I mainly use it with embark.

(define-public emacs-titlecase
  (let ((commit "eb8d23925fb8ccbd3b2e3804fb0a312ee227610b")
        (last-release-version "0.4.1") ;; from the tags in git repo; .el's version is incorrect
        (revision "0")
        (url "https://codeberg.org/acdw/titlecase.el"))
    (package
     (name "emacs-titlecase")
     (version (git-version last-release-version revision commit))
     (source
      (origin
       (method git-fetch)
       (uri (git-reference
             (url (string-append url ".git"))
             (commit commit)))
       (file-name (git-file-name name version))
       (sha256
        (base32
         "1j696incblnqhz7yi8xmshiz2p5kp910288j513sj8rknlykpr4n"))))
     (build-system emacs-build-system)
     (home-page url)
     (synopsis "Titlecase Things in Emacs")
     (description "This library only does it in English, and even then, it's pretty jankily put-together.
Titlecase is the best-effort attempt at capitalizing titles, in English, in Emacs.")
     (license license:gpl3))))
(simple-service
 'home-emacs-titlecase
 home-profile-service-type
 (list
  (specification->package
   "emacs-titlecase")))
(setup (:require titlecase)
  (:also-load embark)
  (:with-map embark-heading-map
    (:bind "T" #'titlecase-line))
  (:with-map embark-region-map
    (:bind "T" #'titlecase-region)))

Eglot

Eglot is Emacs’ built-in LSP client.

(setup eglot
  (with-eval-after-load 'eglot
    (set-face-attribute 'eglot-highlight-symbol-face nil :inherit 'highlight)))

Haskell

(simple-service
 'home-emacs-haskell
 home-profile-service-type
 (list
  (specification->package
   "emacs-haskell-mode")))

Rust

(simple-service
 'home-emacs-rust
 home-profile-service-type
 (list
  (specification->package
   "emacs-eglot")
  (specification->package
   "emacs-rustic-minimal")))

Its package definition introduces lsp-mode and flycheck, so I defines a variant to drop them.

(define-public emacs-rustic-minimal
  (package
    (inherit upstream:emacs-rustic)
    (name "emacs-rustic-minimal")
    (propagated-inputs
     (modify-inputs (package-propagated-inputs upstream:emacs-rustic)
       (delete "emacs-lsp-mode" "emacs-flycheck")))
    (arguments
     `(#:exclude (cons "rustic-flycheck\\.el" %default-exclude)
       ,@(substitute-keyword-arguments (package-arguments upstream:emacs-rustic))))))
(setup (:require rustic)
  (:option rustic-lsp-client 'eglot)
  (:option (prepend display-buffer-alist)
           '((derived-mode . rustic-compilation-mode)
             (display-buffer-reuse-mode-window display-buffer-in-side-window)
             (side . right)
             (slot . 0)
             (window-width . 80)))
  (defun my-rustic-fix-colors ()
    (kill-local-variable 'compilation-message-face)
    (kill-local-variable 'compilation-error-face)
    (kill-local-variable 'compilation-warning-face)
    (kill-local-variable 'compilation-info-face)
    (kill-local-variable 'compilation-column-face)
    (kill-local-variable 'compilation-line-face)
    (kill-local-variable 'xterm-color-names-bright)
    (kill-local-variable 'xterm-color-names))
  (:with-function my-rustic-fix-colors
    (:hook-into rustic-compilation-mode-hook rustic-cargo-spellcheck-mode-hook)))

Dhall

(simple-service
 'home-emacs-dhall
 home-profile-service-type
 (list
  (specification->package
   "dhall")
  (specification->package
   "emacs-dhall-mode-next")))
(define-public emacs-dhall-mode-next
  (let ((commit "87ab69fe765d87b3bb1604a306a8c44d6887681d")
        (last-release-version "0.1.3")
        (revision "0"))
    (package
     (inherit upstream:emacs-dhall-mode)
     (name "emacs-dhall-mode-next")
     (version (git-version last-release-version revision commit))
     (source
      (origin
       (method git-fetch)
       (uri (git-reference
             (url "https://github.com/psibi/dhall-mode")
             (commit commit)))
       (file-name (git-file-name name version))
       (sha256
        (base32
         "1h55bcn0csy7xacl6lqhr3vfva208rszjn15gsfq0pbwhx4n6zhx")))))))

Ron

(define-public emacs-ron-mode
  (let ((commit "c5e0454b9916d6b73adc15dab8abbb0b0a68ea22")
        (last-release-version "1.0.0") ;; from the .el's version
        (revision "0")
        (url "https://codeberg.org/Hutzdog/ron-mode"))
    (package
     (name "emacs-ron-mode")
     (version (git-version last-release-version revision commit))
     (source
      (origin
       (method git-fetch)
       (uri (git-reference
             (url (string-append url ".git"))
             (commit commit)))
       (file-name (git-file-name name version))
       (sha256
        (base32
         "132r5346m3li5n7v7fyzyg8sg3679apl7q4y57n5aq395s0q9wyn"))))
     (build-system emacs-build-system)
     (home-page url)
     (synopsis "Ron-mode for Emacs")
     (description "Syntax highlighting for Rusty Object Notation (RON).")
     (license license:expat))))
(simple-service
 'home-emacs-ron-mode
 home-profile-service-type
 (list
  (specification->package
   "emacs-ron-mode")))

Dart

(simple-service
 'home-emacs-dart
 home-profile-service-type
 (list
  (specification->package
   "emacs-dart-mode-minimal")))

It’s weird that it introduces many unnecessary propagated inputs, so I make a variant to drop them.

(define-public emacs-dart-mode-minimal
  (package
    (inherit upstream:emacs-dart-mode)
    (name "emacs-dart-mode-minimal")
    (propagated-inputs (list))))

PlantUML

(simple-service
 'home-emacs-plantuml
 home-profile-service-type
 (list
  (specification->package
   "emacs-plantuml-mode")))
(setup ob-plantuml
  (:require plantuml-mode)
  (:option org-plantuml-jar-path plantuml-jar-path
           org-plantuml-executable-path plantuml-executable-path
           org-plantuml-exec-mode 'plantuml)
  (with-eval-after-load 'org
    (:option (prepend org-src-lang-modes) '("plantuml" . plantuml))))

Email

mu4e

(service
 (service-type
  (name 'home-mu)
  (extensions
   (list
    (service-extension
     home-profile-service-type
     (const (list
             (specification->package
              "isync")
             (specification->package
              "rss2email")
             (specification->package
              "msmtp")
             (specification->package
              "mu"))))
    (service-extension
     home-environment-variables-service-type
     (const '(("XAPIAN_CJK_NGRAM" . "1"))))))
  (default-value #f)
  (description #f)))
Back-end Initialization

Here is How mu is initialized. This needs to be run manually.

mu init --maildir=<<mu-maildir()>> '--my-address=/<<mu-my-address()>>/'
Front-end Settings
(setup mu4e
  (defun my-mu4e-trash-folder-dispatch (msg)
    (if (and msg
             (string= "/feed" (mu4e-message-field msg :maildir)))
        "/trash-feed"
      "/trash"))
  (:option mu4e-sent-messages-behavior 'delete
           mu4e-headers-auto-update nil
           mu4e-trash-folder #'my-mu4e-trash-folder-dispatch
           mu4e-headers-fields '((:human-date . 12)
                                 (:flags . 6)
                                 (:mailing-list . 10)
                                 (:from-or-to . 22)
                                 (:thread-subject))
           message-kill-buffer-on-exit t
           mail-user-agent 'mu4e-user-agent
           read-mail-command 'mu4e
           mu4e-compose-dont-reply-to-self t
           mu4e-compose-format-flowed t
           mu4e-search-include-related t
           mu4e-change-filenames-when-moving t
           user-mail-address "this@hiecaq.org"
           user-full-name "hiecaq"
           send-mail-function #'sendmail-send-it
           sendmail-program (executable-find "msmtp")
           mail-envelope-from 'header))

Eat

eat is a terminal emulator for Emacs, similar to vterm, but implemented fully in Elisp.

(simple-service
 'home-emacs-eat
 home-profile-service-type
 (list
  (specification->package
   "emacs-eat")))
(setup eat
  (:option eshell-visual-commands nil
           eshell-visual-subcommands nil
           eshell-visual-options nil)
  (eat-eshell-mode +1))

Dired

dired is Emacs’ built-in file explorer. It has a classic text-based UI that is so easy to use that many community-maintained packages follow its design principles.

dired-rsync

One thing that dired (with tramp) does it badly is copying files over network. For small files it is fine, for big files not only is it slow but also it blocks the whole Emacs while copying. dired-rsync to the rescue, which basically wraps rsync and does things asynchronously.

(simple-service
 'home-emacs-dired-rsync
 home-profile-service-type
 (list
  (specification->package
   "emacs-dired-rsync")))
(setup dired-rsync
  (:option dired-rsync-options "-azs --info=progress2"))

References and Recommendations

This configuration is written while referencing the following guix configurations:

About

My literate configuration for Guix

License:GNU General Public License v3.0