- Introduction
- Home Configuration
- Guix
- Cross-Desktop Group (XDG)
- Shells
- Fonts
- Search
- Desktop Environment
- Emacs
- Basics
- Early Initialization
- Main Configurations
- setup.el
- Some Sane Configurations
- Window Management
- Universal Argument
- PCRE
- Help
- Xdg
- No Littering
- Fonts
- Modus Themes
- Mode Line
- Midnight
- Auto Save
- Recentf
- Save History
- Editorconfig
- Envrc
- Subword
- Highlight Parentheses
- Transient
- Evil
- God mode
- Which key
- Posframe
- Eldoc
- Ace Window
- Spell Checking
- Xref
- Topsy
- Orderless
- Vertico
- Marginalia
- Consult
- Embark
- Tempel
- Corfu
- Visual Undo
- Hideshow
- Pulse
- electric-pair-mode
- Aggresive Indent
- Eshell
- Magit
- Project
- Emacsql
- Epub
- Org Mode
- Detached
- English
- Eglot
- Haskell
- Rust
- Dhall
- Ron
- Dart
- PlantUML
- MPV
- PYIM
- vterm
- Eat
- Dired
- Guix
- Desktop Notification Daemon
- References and Recommendations
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.
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"
)))
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
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)))
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
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.
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")))
(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 (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
(service
home-fish-service-type)
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)))
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 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)))
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")))
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 is the environment switcher on the shell level, based on current directories.
(simple-service
'home-direnv
home-profile-service-type
(list
(specification->package
"direnv")))
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"
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
(define-module (hiecaq services search)
#:use-module (guix gexp)
#:use-module (guix packages)
#:use-module (gnu services)
#:use-module (gnu services configuration)
#:use-module (gnu packages search)
#:use-module (gnu system shadow) ;; account-service-type
#:use-module (ice-9 match)
#:use-module (ice-9 string-fun)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-26)
#:export (locate-configuration
locate-configuration?
locate-configuration-locate
locate-configuration-fields
locate-service-type))
(define (uglify-field-name field-name)
(let* ((str (symbol->string field-name))
(up (string-upcase str)))
(if (string-suffix? "?" up)
(string-replace-substring (string-drop-right up 1) "-" "_")
(string-replace-substring up "-" ""))))
(define (strings? lst)
(every string? lst))
(define (serialize-field field-name value)
#~(string-append #$(uglify-field-name field-name)
" = \""
#$value
"\"\n"))
(define (serialize-strings field-name strs)
(serialize-field field-name (string-join strs " ")))
(define (serialize-boolean field-name value)
(serialize-field field-name (if value "yes" "no")))
(define serialize-group empty-serializer)
(define (group? s) (string? s))
(define-maybe strings)
(define-maybe boolean)
(define-maybe group)
(define-configuration locate-configuration
(locate
(package plocate)
"The locate package to use.")
(group
(group "locate")
"Locate group used to run updatedb.")
(prune-fs
maybe-strings
"List of file system types (as used in /etc/mtab) which should not be scanned.")
(prune-names
maybe-strings
"List of directory names (without paths) which should not be scanned.")
(prune-paths
maybe-strings
"List of directory absolute paths which should not be scanned.")
(prune-bind-mounts?
maybe-boolean
"If true, bind mounts are not scanned."))
(define (locate-etc config)
`(("updatedb.conf" ,(mixed-text-file
"updatedb.conf"
"# Generated by 'locate-service'.\n"
(serialize-configuration
config locate-configuration-fields)))))
(define (locate-group config)
(list
(user-group
(name (locate-configuration-group config))
(system? #t))))
(define locate-service-type
(service-type
(name 'locate)
(extensions
(list (service-extension profile-service-type (compose list locate-configuration-locate))
(service-extension etc-service-type locate-etc)
(service-extension account-service-type locate-group)))
(default-value (locate-configuration))
(description #f)))
My “desktop environment” is plain window management with friends.
(define-module (hiecaq home de)
#:use-module (guix gexp)
#:use-module (gnu services)
#:use-module (gnu home services)
<<de-use-module>>)
(define-public services
(list
<<de-service>>))
((hiecaq home de) #:prefix de:)
de:services
I currently use Guix’s default display manager, i.e. gdm
, and when there is no *.desktop
of WMs available in its search path, it can log in with the user provided ~/.xsession
executable (which won’t be displayed in the selection menu).
So, simply
exec xmonad
#:use-module (hiecaq packages wm)
(service
(service-type
(name 'home-wm)
(extensions
(list
(service-extension
home-profile-service-type
(const (list
xmonad
ghc-xmonad-contrib
xmobar)))
(service-extension
home-files-service-type
;; recursive to keep x bits, see https://lists.gnu.org/archive/html/help-guix/2023-03/msg00190.html
(const `((".xsession" ,(local-file "../../xsession" #:recursive? #t)))))))
(default-value #f)
(description #f)))
(define-module (hiecaq packages wm)
#:use-module (guix utils)
#:use-module (guix build-system haskell)
#:use-module (guix packages)
#:use-module (guix download)
#:use-module (gnu packages haskell)
#:use-module ((gnu packages haskell-xyz) #:prefix upstream:)
#:use-module ((gnu packages wm) #:prefix upstream:))
(define-public xmonad
(package
(inherit upstream:xmonad)
(name "xmonad")
(version "0.18.0")
(source
(origin
(method url-fetch)
(uri (hackage-uri "xmonad" version))
(sha256
(base32 "1ysxxjkkx2l160nlj1h8ysxrfhxjlmbws2nm0wyiivmjgn20xs11"))))))
(define-public ghc-xmonad-contrib
(package
(inherit upstream:ghc-xmonad-contrib)
(name "ghc-xmonad-contrib")
(version "0.18.1")
(source
(origin
(method url-fetch)
(uri (hackage-uri "xmonad-contrib" version))
(sha256
(base32 "0ck4hq9yhdzggrs3q4ji6nbg6zwhmhc0ckf9vr9d716d98h9swq5"))))
(inputs
(modify-inputs (package-inputs upstream:ghc-xmonad-contrib)
(replace "xmonad" xmonad)))
(arguments (substitute-keyword-arguments
(package-arguments upstream:ghc-xmonad-contrib)
((#:cabal-revision cr) #f)))))
(define-public ghc-xmobar
(package
(inherit upstream:ghc-xmobar)
(version "0.48.1")
(source
(origin
(method url-fetch)
(uri (hackage-uri "xmobar" version))
(sha256
(base32 "1infcisv7l00a4z4byjwjisg4yndk0cymibfii1c7yzyzrlvavhl"))))
(inputs
(modify-inputs (package-inputs upstream:ghc-xmobar)
(prepend upstream:ghc-extra)))))
(define-public xmobar
(package
(inherit upstream:xmobar)
(version "0.48.1")
(source
(origin
(method url-fetch)
(uri (hackage-uri "xmobar" version))
(sha256
(base32 "1infcisv7l00a4z4byjwjisg4yndk0cymibfii1c7yzyzrlvavhl"))))
(inputs
(modify-inputs (package-inputs upstream:xmobar)
(replace "ghc-xmobar" ghc-xmobar)))))
Start a session-specific D-Bus for unprivileged apps:
#:use-module (gnu home services desktop)
(service home-dbus-service-type)
xdg-desktop-portal exposes a series of D-bus interface to give sandboxed application access to some host system functionalities, most notably file-picker, in a way similar to Android nowadays.
There are several daemons involved, and all of them will be automatically started the first time related D-bus events happen:
xdg-desktop-portal
, the daemon provides the API that application interacts with.xdg-document-portal
, the daemon that binds the shared files inside and outside sandboxes, i.e under/run/usr/$UID/doc
xdg-permission-store
, the daemon that keeps the permissions which a user has given to apps.xdg-desktop-portal-gtk
, the daemon that is the back-end that actually handles the translated and standardized requests.
BTW, if the portal does not work immediately after reconfigure, try reboot the system.
#:use-module (gnu packages freedesktop)
(service
(service-type
(name 'home-xdg-desktop-portal)
(extensions
(list
(service-extension
home-profile-service-type
(const (list xdg-desktop-portal
xdg-desktop-portal-gtk)))
(service-extension
home-xdg-configuration-files-service-type
(const `(("xdg-desktop-portal/portals.conf" ,(local-file "../../portals.conf")))))))
(default-value #f)
(description #f)))
The following file set using xdg-desktop-portal-gtk
as the default backend. There can actually be multiple backends running at the same time.
[preferred]
default=gtk
I use a user pipewire session.
#:use-module (gnu home services sound)
(service home-pipewire-service-type)
Implement a home-emacs-service-type
that
- The service itself defines the Emacs version to use and the “Emacs compiler” to use, via
home-emacs-configuration
- The service’s extension add Emacs packages to use, configuration file to link, etc, via
home-emacs-extension
.
The reason for this set-up is
- I can easily swap between different Emacs versions, and packages will be automatically transformed to using that version’s byte-codes.
- Configurations are discrete by using extensions, so they fit this literature configuration set-up better.
(define-module (hiecaq home services emacs)
#:use-module (gnu services)
#:use-module (gnu services configuration)
#:use-module (gnu home services)
#:use-module ((gnu packages emacs) #:prefix upstream:)
#:use-module (guix packages)
#:use-module (srfi srfi-1)
#:export (home-emacs-configuration
home-emacs-extension
home-emacs-service-type))
(define-configuration/no-serialization home-emacs-configuration
(emacs
(package upstream:emacs)
"Emacs to use.")
(emacs-compiler
(package upstream:emacs-minimal)
"Emacs used for compiling packages.")
(packages
(list '())
"List of Emacs packages to use.")
(configs
(alist '())
"Emacs configuration files."))
(define (home-emacs-transformed-package config)
(package-input-rewriting
`((,upstream:emacs-minimal
. ,(home-emacs-configuration-emacs-compiler config))
(,upstream:emacs-no-x
. ,(home-emacs-configuration-emacs config))
(,upstream:emacs
. ,(home-emacs-configuration-emacs config)))))
(define (home-emacs-profile config)
`(,(home-emacs-configuration-emacs config)
,@(map (home-emacs-transformed-package config)
(home-emacs-configuration-packages config))))
(define-configuration/no-serialization home-emacs-extension
(packages
(list '())
"Extra list of Emacs packages to use.")
(configs
(alist '())
"Extra Emacs configuration files."))
(define (home-emacs-extensions original-config extension-configs)
(let ((append-fields
(lambda (config-getter extension-getter)
(append (config-getter original-config)
(append-map extension-getter extension-configs)))))
(home-emacs-configuration
(inherit original-config)
(packages (append-fields home-emacs-configuration-packages
home-emacs-extension-packages))
(configs (append-fields home-emacs-configuration-configs
home-emacs-extension-configs)))))
(define home-emacs-service-type
(service-type
(name 'home-emacs)
(extensions
(list (service-extension home-xdg-configuration-files-service-type
home-emacs-configuration-configs)
(service-extension home-profile-service-type
home-emacs-profile)
(service-extension home-environment-variables-service-type
(const '(("EDITOR" . "emacsclient -a nvim -c")
("VISUAL" . "emacsclient -a nvim -c"))))))
(compose identity)
(extend home-emacs-extensions)
(default-value (home-emacs-configuration))
(description #f)))
(define-module (hiecaq home emacs)
#:use-module (gnu services)
#:use-module (gnu packages)
#:use-module ((gnu packages emacs) #:prefix upstream:)
#:use-module (gnu home services)
#:use-module (gnu home services shells)
#:use-module (hiecaq home services emacs)
#:use-module (guix gexp))
(define-public services
(list
<<emacs-service>>))
Add this module and its services:
((hiecaq home emacs) #:prefix emacs:)
emacs:services
I’m currently using emacs
from Guix official channel.
(service home-emacs-service-type
(home-emacs-configuration
(emacs upstream:emacs-next)
(emacs-compiler upstream:emacs-next-minimal)))
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 gexp)
#: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.
(simple-service
'home-emacs-early-init
home-emacs-service-type
(home-emacs-extension
(configs `(("emacs/early-init.el" ,(local-file "../../early-init.el"))))))
;;; early-init.el --- Configurations before package systems and UI systems -*- lexical-binding: t; buffer-read-only: t; eval: (auto-revert-mode 1) -*-
I don’t use the built-in package.el
to fetch packages, so I’ll turn it off:
(setq package-enable-at-startup nil)
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])))))
load
prefers the newest version of a file (when suffix is not given).
(setq load-prefer-newer t)
(setq load-no-native t)
(simple-service
'home-emacs-init
home-emacs-service-type
(home-emacs-extension
(configs `(("emacs/init.el" ,(local-file "../../init.el"))))))
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 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-emacs-service-type
(home-emacs-extension
(packages
(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))
(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 108))
Emacs comes with a customization interface, which supports setting via function calls too (good!) and saves the results in a file (bad!). Up until Emacs 29, I set the storage to /dev/null
. Started from Emacs 30, I find that sometimes file-defined local variables are not loaded the first time I open a buffer, so I came up with a new solution: set it to a random temporary file every time Emacs starts.
(setup cus-edit
(:option custom-file null-device)
(defun my-custom-file-set ()
(:option custom-file
(make-temp-file "emacs-custom-" nil ".el"
";; auto-generated by custom-file\n")))
(:with-function my-custom-file-set
(:hook-into after-init)))
Allow word-wrap at any CJK character, otherwise it only wraps at spaces when there are also non-CJK characters in the physical lines, producing sparse visual lines.
(setup emacs
(:option word-wrap-by-category t))
Also, Emacs by default auto-renames certain buffers when a buffer with the same name is killed, which brings trouble to scripting. So I’d have this feature turned off.
(setup uniquify
(:option uniquify-after-kill-buffer-p nil))
(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))))
(defun my-window-shot (&optional window)
"Take screenshot of a given Emacs window."
(interactive)
(pcase-let ((`(,window-left ,window-top ,window-right ,window-bottom)
(window-edges (window-normalize-window window t) nil t t)))
(let* ((geo (format "%dx%d+%d+%d"
(- window-right window-left)
(- window-bottom window-top)
window-left
window-top))
(file (expand-file-name (format "%f.jpg" (time-to-seconds (time-since 0)))
(xdg-user-dir "PICTURES"))))
(make-process :name "window-shot"
:command `("maim"
"-m" "10"
"--geometry" ,geo
,file)))))
(defvar my-window-record--process nil "Running record process")
(defun my-window-record (&optional window sec)
"Take screen record of a given Emacs window."
(interactive)
(if (process-live-p my-window-record--process)
(process-send-string my-window-record--process "q")
(pcase-let ((`(,window-left ,window-top ,window-right ,window-bottom)
(window-edges (window-normalize-window window t) nil t t)))
(let* ((size (format "%dx%d"
(- window-right window-left)
(- window-bottom window-top)))
(geo (format ":0.0+%d,%d"
window-left
window-top))
(file (expand-file-name (format "%f.mp4" (time-to-seconds (time-since 0)))
(xdg-user-dir "PICTURES")))
(proc (make-process :name "window-record"
:buffer "*window-record*"
:connection-type 'pty
:command `("ffmpeg"
"-video_size" ,size
"-framerate" "8"
"-f" "x11grab"
"-i" ,geo
,file))))
(setq my-window-record--process proc)
(unless (null sec)
(run-with-timer sec nil
#'process-send-string proc "q"))))))
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))))
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-pcre
home-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-pcre2el")))))
TODO: this should not require help.
(setup (:require help)
(:global (:unbind "C-h C-h")))
(setup (:require xdg))
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-no-littering
home-emacs-service-type
(home-emacs-extension
(packages
(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)
(setq exdg-config-dir (expand-file-name "config/" user-emacs-directory))
(set-face-attribute 'default nil :height 140)
(set-face-attribute 'variable-pitch nil :weight 'normal :inherit 'default)
(when (eq system-type 'gnu/linux)
(set-face-attribute 'default nil :family "Hack")
(set-face-attribute 'variable-pitch nil :family "Sans Serif"))
(set-face-attribute 'fixed-pitch nil :family (internal-get-lisp-face-attribute 'default :family))
(simple-service
'home-emacs-modus-themes
home-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-modus-themes")))))
(setup modus-themes
(:option modus-themes-mixed-fonts t)
(:require modus-themes)
(load-theme 'modus-vivendi :no-confirm))
(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
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))
(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 is an Emacs built-in minor mode that saves recent file list.
(setup recentf
(:option recentf-save-file (exdg-state "recentf-save.el"))
(:enable))
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 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. It is built-in since Emacs 30.
(setup editorconfig
(:enable))
envrc is Emacs’ integration with direnv that works in buffer-local style.
(simple-service
'home-emacs-envrc
home-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-envrc")
(specification->package
"emacs-inheritenv")))))
(setup envrc
(:also-load inheritenv)
(:with-mode envrc-global-mode
(:hook-into after-init)))
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, 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-emacs-service-type
(home-emacs-extension
(packages
(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)))
(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")))
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:
- evil-guide by noctuid
(simple-service
'home-emacs-evil
home-emacs-service-type
(home-emacs-extension
(packages
(map specification->package
(list
"emacs-goto-chg"
"emacs-evil"
"emacs-evil-collection-next"
"emacs-evil-surround"
"emacs-evil-snipe"
"emacs-evil-commentary")))))
annalist is a dependency of emacs-evil-collection
, and its test dependency lispy somehow fail to build under Emacs 30 because of test failures. I simply disable tests for annalist
and deletes all its test dependencies.
(define-public emacs-annalist-minimal
(package
(inherit upstream:emacs-annalist)
(name "emacs-annalist-minimal")
(native-inputs '())
(arguments (substitute-keyword-arguments
(package-arguments upstream:emacs-annalist)
((#:tests? t) #f)))))
I need some latest contributions to the evil-collection
repository:
(define-public emacs-evil-collection-next
(let ((commit "20c415aaa07c6541753489b166cd58d6771bd1e1")
(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
"17ifxk4lpj1l52b3m2x5sj5ywdnrjyy1hbvfbvg4zwa1kc0l3ds1"))))
(propagated-inputs
(modify-inputs (package-propagated-inputs upstream:emacs-evil-collection)
(replace "emacs-annalist" emacs-annalist-minimal))))))
(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 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 defines a replace
operator. However, we can implement its functionality easily with Evil mode itself, see this post. I add some simple code to the solution there to make ""
register work as the way I want.
(evil-define-operator my-evil-replace-with-register (count beg end type register)
"Replacing an existing text with the contents of a register"
:move-point nil
(interactive "<vc><R><x>")
(setq count (or count 1))
(let ((saved (evil-get-register ?\")))
(if (eq type 'block)
(evil-visual-paste count register)
(delete-region beg end)
(evil-paste-before count register))
(evil-set-register ?\" saved)))
(setup evil
(:global (:with-state (normal visual)
(:bind "," #'my-evil-replace-with-register))))
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 defines operators for commenting.
(setup evil-commentary
(:also-load evil)
(:enable))
Add my helper commands to the evil-window-map
(setup evil
(:with-map evil-window-map
(:bind "M-s" #'my-window-shot
"M-r" #'my-window-record)))
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-god-mode
home-emacs-service-type
(home-emacs-extension
(packages
(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 is a minor mode that hints you the keybindings prefixed with what you have typed when you get stuck. It is built-in since Emacs 30.
I turned off which-key-show-transient-maps
because it has cause embark-act
on a non-minibuffer target to behave strangely when the binding in keymap is longer than a single key:
- Embark loses focus on the minibuffer (and is captured to the window containing the target) if
embark-prefix-help-command
is queried after giving the first key embark-prefix-help-command
cannot shows the correct keymap after the first key is given
(setup which-key
(:option which-key-show-transient-maps nil))
(setup (:require which-key)
(:option 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 pops a child-frame at point, connected to its root window’s buffer.
(simple-service
'home-emacs-posframe
home-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-posframe")))))
(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-emacs-service-type
(home-emacs-extension
(packages
(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 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-emacs-service-type
(home-emacs-extension
(packages
(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))
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)))
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
home-emacs-service-type
(home-emacs-extension
(packages
(map specification->package
'("hunspell"
"hunspell-dict-en-us"
"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
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 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-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-topsy")))))
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-emacs-service-type
(home-emacs-extension
(packages
(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 defaultcompletion-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 “provides a performant and minimalistic vertical completion UI based on the default completion system.”
(simple-service
'home-emacs-vertico
home-emacs-service-type
(home-emacs-extension
(packages
(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 adds info to the right of completion candidates, thus the name margin-alia.
(simple-service
'home-emacs-marginalia
home-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-marginalia")))))
(setup (:require marginalia)
(:enable))
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-consult
home-emacs-service-type
(home-emacs-extension
(packages
(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 "plocate --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 (cadr (orderless-compile 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 (cdr (orderless-compile 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))
consult-info
can be used as a Info-search
drop-in replacement:
(setup info
(:with-mode Info-mode
(:rebind #'Info-search #'consult-info
#'Info-search-case-sensitively #'consult-info)))
(setup consult
(defun consult-info-emacs ()
"Search through Emacs info pages."
(interactive)
(consult-info "emacs" "efaq" "elisp" "eintr" "cl"))
(defun consult-info-org ()
"Search through the Org info page."
(interactive)
(consult-info "org" "orgguide" "org-roam" "org-super-agenda"))
(defun consult-info-completion ()
"Search through completion info pages."
(interactive)
(consult-info "vertico" "consult" "marginalia" "orderless" "embark"
"corfu" "tempel"))
(defun consult-info-guix ()
"Search through guix info pages."
(interactive)
(consult-info "guix" "guix-cookbook" "emacs-guix" "guile")))
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-emacs-service-type
(home-emacs-extension
(packages
(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,
After using this set-up for a while, I found it quite annoying that it requires hitting RET
after filtering to pick targets. This can be fixed with this advice:
(define-advice vertico--update (:after (&rest _) choose-filtered-target)
"Pick the target when input has filtered candidates to only one."
(when (and (eq vertico--total 1)
(eq (vertico--metadata-get 'category) 'embark-target)
(> (cdr vertico--input) 0))
(vertico-exit)))
(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 and ace-window-prefix, 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
(defun ace-window-prefix ()
"Use `ace-window' to display the buffer of the next command.
The next buffer is the buffer displayed by the next command invoked
immediately after this command (ignoring reading from the minibuffer).
Creates a new window before displaying the buffer.
When `switch-to-buffer-obey-display-actions' is non-nil,
`switch-to-buffer' commands are also supported."
;; steal from https://karthinks.com/software/emacs-window-management-almanac/#a-window-prefix-command-for-ace-window
(interactive)
(display-buffer-override-next-command
(lambda (buffer _)
(let (window type (aw-dispatch-always t))
(setq
window (aw-select (propertize " ACE" 'face 'mode-line-highlight))
type 'reuse)
(cons window type)))
nil "[ace-window]")
(message "Use `ace-window' to display next command buffer..."))
(defvar my-embark-prefix-commands '(ace-window-prefix other-window-prefix)
"Commands that should be considered as a prefix command.")
(defun my-embark-is-prefix-command (cmd)
(memq cmd my-embark-prefix-commands))
(define-advice embark-keymap-prompter (:around (orig-fun keymap update) handle-prefix-command)
"Don't use prefix command as embark action."
(let ((cmd (funcall orig-fun keymap update)))
(pcase cmd
((pred my-embark-is-prefix-command)
(ignore-errors (command-execute cmd))
(embark-keymap-prompter keymap update))
(_ cmd))))
(:global
(:with-state (normal visual)
(:bind "M-o" #'ace-window-prefix)))
(:with-map vertico-map
(:bind "M-o" #'ace-window-prefix))
(:with-map embark-meta-map
(:bind "M-o" #'ace-window-prefix)))
Note: Somehow only post-hooks can recognize (minibufferp)
.
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-emacs-service-type
(home-emacs-extension
(packages
(map specification->package
'("emacs-tempel"
"emacs-eglot-tempel")))
(configs `(("emacs/config/templates.eld" ,(local-file "../../templates.eld"))))))
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"))
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.
(setup eglot-tempel
(:hook-into eglot-server-initialized-hook))
corfu is a completion-at-point
implementation that is much more concise than company
.
(simple-service
'home-emacs-corfu
home-emacs-service-type
(home-emacs-extension
(packages
(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)))
vundo is basically a less-buggy undo-tree that supports Emacs 28’s new undo-redo
.
(simple-service
'home-emacs-vundo
home-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-vundo")))))
(setup (:require vundo)
(:with-map my-leader-map (:bind "u" #'vundo)))
hideshow is Emacs’ built-in code folding package.
(setup hideshow
(:with-mode hs-minor-mode (:hook-into prog-mode)))
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
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)))
aggressize-indent-mode basically reindents what you have changed after every change you made.
(simple-service
'home-emacs-aggressive-indent
home-emacs-service-type
(home-emacs-extension
(packages
(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))
I plan on switching to eshell
as my main shell. Here are some references:
- Why Use Eshell? by Howard Abrams
- Eshell as a main shell (web archived) by Pierre Neidhardt
- BASH, ZSH, FISH. How about Eshell? from TRITON FAMME
- Discussion on Tweaking Eshell on Emacs China (in Chinese)
- Mastering Eshell by Mickey Petersen
- Introductions to Emacs Builtin Mode Features from
emacs-newbie
(in Chinese) - Use
hs-minor-mode
Where It’s Not Supported asked on Emacs Stack Exchange - Customizing Your Emacs Eshell Prompt by Liang Zan
- Eshell Category on Emacs Wiki
- Eshell’s Offical Manual
(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))))
Eshell by default don’t bind C-d
to quitting the shell window and process. To do this, I steal
a snippet from Howard Abrams and modified it a little bit to use the new Emacs 30 features.
(setup eshell
(defun my-eshell-quit-or-delete-char (arg)
(interactive "p")
(if-let* (((eolp))
(point (save-excursion
(when-let* ((match (text-property-search-backward 'field 'prompt t)))
(goto-char (prop-match-end match)))))
((eq point (point-max))))
(progn
(insert "exit")
(eshell-send-input))
(delete-forward-char arg)))
(:with-state insert (:bind "C-d" #'my-eshell-quit-or-delete-char)))
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-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-fish-completion")))))
(setup fish-completion
(:needs "fish")
(:hook-into eshell-mode))
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-emacs-service-type
(home-emacs-extension
(packages
(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.
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
"v" #'my-project-vterm
"s" #'my-project-vterm-command
"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 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")
(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-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-emacsql-sqlite-builtin")))))
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-emacs-service-type
(home-emacs-extension
(packages
(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)))
For PDF files, Emacs’ built-in doc-view mode is actually quite usable. It pre-renders the PDF files into images and save them in the filesystem.
Anyway, I use pdf-tools which relies on an external program epdfinfo
that utilizes poppler. The pages are rendered on-demand and stored in memory only, and more importantly it provides some extra features, such as the support for PDF markup annotations.
(simple-service
'home-emacs-pdf-tools
home-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-pdf-tools")))))
Its Guix package already handles the pdfinfo
program, so I set it up without letting it re-attempt the build-on-the-fly process.
(setup pdf-tools
(pdf-loader-install nil t))
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-emacs-service-type
(home-emacs-extension
(packages
(map specification->package
'("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))
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)
Disable org-mode’s own window arrangement when editing source block and have it just use display-buffer
. With this way, the window control is left to display-buffer-alist
.
(:option org-src-window-setup 'plain)
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))
I generally follow the GTD way as my task management system.
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)
Don’t open files in new window. Let the display-buffer-alist
decides instead.
(setup ol
(:when-loaded
(:option (prepend org-link-frame-setup) '(file . find-file))))
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))
References:
- Babel: Introduction in worg/org-contrib
- Working with Source Code from
org-mode
’s manual - Introduction to Literate Programming by Howard Abrams
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 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))
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.
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")))
Org-mode has built-in manpage link support, but it is not on by default:
(setup (:require ol-man))
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 #'embark-open-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 basically does 2 things:
- Use a sqlite database to cache everything that is getting slow as notes scaling up
- 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-emacs-service-type
(home-emacs-extension
(packages
(map specification->package
'("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 ?n
: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))))))
(defvar my-global-bibliography
(list (expand-file-name "notes/refs.bib" (xdg-user-dir "DOCUMENTS")))
"A list to global bib files.")
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-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"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)))
There are quite a lot bibliographic packages, among which I use citar.
(simple-service
'home-emacs-citar
home-emacs-service-type
(home-emacs-extension
(packages
(map specification->package
'("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
citar-library-paths `(,(expand-file-name "resources" (xdg-user-dir "DOCUMENTS")))
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))
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, 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"))
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 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-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-detached")))))
(setup detached
(:option detached-init-block-list '(dired-rsync))
(: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))))))))
(simple-service
'home-emacs-flymake-vale
home-emacs-service-type
(home-emacs-extension
(packages
(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:vale)))))
(setup flymake-vale
(:with-function flymake-vale-load
(:hook-into text-mode)))
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-emacs-service-type
(home-emacs-extension
(packages
(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 is Emacs’ built-in LSP client.
(setup eglot
(with-eval-after-load 'eglot
(set-face-attribute 'eglot-highlight-symbol-face nil :inherit 'highlight)))
(simple-service
'home-emacs-haskell
home-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-haskell-mode")))))
(simple-service
'home-emacs-rust
home-emacs-service-type
(home-emacs-extension
(packages
(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)))
(simple-service
'home-emacs-dhall
home-emacs-service-type
(home-emacs-extension
(packages
(map specification->package
'("dhall"
"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")))))))
(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
home-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-ron-mode")))))
(simple-service
'home-emacs-dart
home-emacs-service-type
(home-emacs-extension
(packages
(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))))
(simple-service
'home-emacs-plantuml
home-emacs-service-type
(home-emacs-extension
(packages
(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))))
(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"))))
(service-extension
home-emacs-service-type
(const (home-emacs-extension
(packages
(list
(specification->package
"mu"))))))
(service-extension
home-environment-variables-service-type
(const '(("XAPIAN_CJK_NGRAM" . "1"))))))
(default-value #f)
(description #f)))
Here is How mu
is initialized. This needs to be run manually.
mu init --maildir=<<mu-maildir()>> '--my-address=/<<mu-my-address()>>/'
(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))
mu4e-thread-fold-unread t
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 "[email protected]"
user-full-name "hiecaq"
send-mail-function #'sendmail-send-it
sendmail-program (executable-find "msmtp")
message-send-mail-function 'message-send-mail-with-sendmail
mail-envelope-from 'header)
(:with-map mu4e-headers-mode-map
(:with-state (normal)
(:bind "T" #'mu4e-view-mark-thread))))
mpv.el controls mpv via its IPC interface, useful for note-taking.
(simple-service
'home-emacs-mpv
home-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-mpv")))))
Advice org-timer-item
so that when mpv is running it inserts the mpv timestamp, instead of starting a new timer. This is a different approach than mpv-insert-playback-position
(but it is based on its implementation), because I think this way gives better compatibility (e.g evil-org-open-below
)
(setup org
(org-link-set-parameters "mpv"
:follow (lambda (file)
(if (mpv--url-p file)
(mpv-play-url file)
(mpv-play file))))
(define-advice org-timer-item (:around (orig-fun &rest r) insert-mpv-timestamp)
"Insert mpv timestamp instead if mpv is running."
(if-let* (((mpv-live-p))
(time (mpv-get-playback-position))
(hms (org-timer-secs-to-hms (round time))))
(cl-letf (((symbol-function 'org-timer)
(lambda (&optional _restart no-insert)
(funcall
(if no-insert #'identity #'insert)
(concat hms " ")))))
(apply orig-fun r))
(apply orig-fun r))))
Add mpv-seek-to-position-at-point
to org-open-at-point-functions
on demand:
(setup mpv
(:with-hook mpv-on-start-hook
(:hook (lambda (&rest r) (add-hook 'org-open-at-point-functions
#'mpv-seek-to-position-at-point))))
(:with-hook mpv-on-exit-hook
(:hook (lambda () (remove-hook 'org-open-at-point-functions
#'mpv-seek-to-position-at-point)))))
Define an embark target and keymap, which can be used when mpv is running.
(defun embark-target-mpv ()
(when (and (fboundp #'mpv-live-p) (mpv-live-p))
(cons 'mpv (mpv-get-property "filename/no-ext"))))
(add-hook 'embark-target-finders #'embark-target-mpv 10)
(defvar-keymap embark-mpv-map
:doc "Commands to act on current mpv process."
:parent embark-general-map
"RET" #'mpv-pause
"q" #'mpv-quit
"k" #'mpv-kill
"f" #'mpv-seek-forward
"b" #'mpv-seek-backward
"-" #'mpv-volume-decrease
"+" #'mpv-volume-increase)
(setup embark
(:when-loaded
(:option (prepend embark-keymap-alist) '(mpv . embark-mpv-map)
(prepend* embark-repeat-actions)
'(mpv-seek-forward
mpv-seek-backward
mpv-volume-decrease
mpv-volume-increase))))
pyim is an Emacs input method framework for Chinese, similar to fcitx for Linux, which also provides some Chinese-related parsing utilities.
(simple-service
'home-emacs-pyim
home-emacs-service-type
(home-emacs-extension
(packages
(map specification->package
'("emacs-pyim"
"emacs-pyim-basedict")))))
(setup (:require pyim)
(:require pyim-basedict)
(pyim-basedict-enable)
(:option default-input-method "pyim"
pyim-dcache-directory (exdg-cache "pyim/")
pyim-page-length 5
pyim-default-scheme 'quanpin))
PYIM use 1-based index, which is not very friendly to Dvorak-programmer keymaps. There is no configuration available currently, so I override pyim-page-menu-create
with a slightly modified version of it.
(setup pyim
(define-advice pyim-page-menu-create
(:override (candidates position &optional separator hightlight-current)
my-0-base)
"Overwrite the target to use 0-based index."
(let ((i 0) result)
(dolist (candidate candidates)
(let ((str (substring-no-properties
(if (consp candidate)
(concat (car candidate) (cdr candidate))
candidate))))
;; highlight for `pyim-page-next-word'
(push
(if (and hightlight-current
(= i position))
(format "%d%s" i
(propertize
(format "[%s]" str)
'face 'pyim-page-selection))
(format "%d.%s " i str))
result)
(setq i (1+ i))))
(string-join (nreverse result) (or separator "")))))
Map Dvorak programmer number key rows to the number they corresponding to, so that I don’t need to use Shift key when selecting words.
(setup pyim
(dolist (d (number-sequence 0 9))
(:bind (alist-get d my-dvp-digit-row-alist)
(lambda ()
(interactive)
(pyim-select-word-by-number (1+ d))))))
(simple-service
'home-emacs-vterm
home-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-vterm")))))
(simple-service
'fish-vterm-setup
home-fish-service-type
(home-fish-extension
(config
(list (mixed-text-file "fish-vterm.fish"
"if test \"$INSIDE_EMACS\" = 'vterm';"
" source $EMACS_VTERM_PATH/etc/emacs-vterm.fish;"
"end")))))
(setup vterm
(:option vterm-shell "~/.guix-home/profile/bin/fish")
(:unbind "C-SPC")
(defun my-project-vterm ()
"Start a project-specific vterm buffer, or switch to the existing one."
(interactive)
(defvar vterm-buffer-name)
(let* ((default-directory (project-root (project-current t)))
(vterm-buffer-name (project-prefixed-buffer-name "vterm"))
(vterm-buffer (get-buffer vterm-buffer-name)))
(if (and vterm-buffer (not current-prefix-arg))
(pop-to-buffer vterm-buffer (bound-and-true-p display-comint-buffer-action))
(vterm t))))
(defun my-project-vterm-command (cmd)
(interactive
(list (read-shell-command (if shell-command-prompt-show-cwd
(format-message "Shell command in `%s': "
(abbreviate-file-name
default-directory))
"Shell command: ")
nil nil)))
(unless (string-empty-p cmd)
(with-current-buffer (call-interactively #'my-project-vterm)
(vterm-send-string (concat cmd "\n"))
(when (evil-normal-state-p)
(evil-collection-vterm-insert))))))
eat is a terminal emulator for Emacs, similar to vterm
, but implemented fully in Elisp.
(simple-service
'home-emacs-eat
home-emacs-service-type
(home-emacs-extension
(packages
(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
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.
(setup dired
(:hook #'dired-hide-details-mode)
(:option dired-dwim-target t
dired-listing-switches "-alh"))
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-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-dired-rsync")))))
(setup dired-rsync
(:option dired-rsync-options "-azs --info=progress2"))
(simple-service
'home-emacs-guix
home-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-guix")))))
ednc enables Emacs to be the desktop notification daemon listening D-bus, similar to Dunst.
(simple-service
'home-emacs-ednc
home-emacs-service-type
(home-emacs-extension
(packages
(list
(specification->package
"emacs-ednc")))))
The following configuration let a posframe show up whenever there are new notifications. When the posframe is presented, I can use C-g
to dismiss the latest notification, and when there is none the posframe will be automatically closed (well, invisible actually).
(setup ednc
(defvar my-ednc-posframe--buffer "*ednc-posframe*"
"Buffer used for ednc notification posframe display.")
(defun my-ednc-posframe-show ()
(interactive)
(when (and (buffer-live-p (get-buffer my-ednc-posframe--buffer))
(posframe-workable-p))
(posframe-show my-ednc-posframe--buffer
:poshandler #'posframe-poshandler-frame-top-center
:border-width 1)))
(defun my-ednc-posframe-hide ()
(interactive)
(when (posframe-workable-p)
(posframe-hide my-ednc-posframe--buffer)))
(defun my-ednc-posframe--update (&rest _)
(let ((notifications (ednc-notifications)))
(with-current-buffer (get-buffer-create my-ednc-posframe--buffer)
(erase-buffer)
(insert (mapconcat
(lambda (n) (ednc-format-notification n :expand))
notifications "")))
(when (posframe-workable-p)
(if notifications
(my-ednc-posframe-show)
(my-ednc-posframe-hide)))))
(defun my-ednc--dismiss-first-notification ()
(when-let* ((buffer (get-buffer my-ednc-posframe--buffer))
(frame (with-current-buffer buffer
posframe--frame))
((frame-visible-p frame))
(notification (ednc-notifications)))
(ednc-dismiss-notification (car notification))))
(advice-add #'keyboard-quit :before #'my-ednc--dismiss-first-notification)
(:when-loaded
(:with-hook ednc-notification-presentation-functions
(:hook #'my-ednc-posframe--update)))
(:enable))
This configuration is written while referencing the following guix configurations:
- Aleksandr Vityazev’s Guix Configuration
- Dustin Lyon’s Literate Configuration for Guix Linux
- iyzsong’s Guix System and Home Config
- Nikita Domnitskii’s Dotfiles
- Nicolas Graves’s Dotfiles
- Luhux 的 Guix 操作系统配置文件
- rde by Andrew Tropin. I use it as a channel.
- Tumashu’s Guixsd Configuration
- Rosenthal by Hako, a Guix channel
- Yves Zoundi’s Guix Configuration