Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/hiecaq/guix-config
My literate configuration for Guix
https://github.com/hiecaq/guix-config
dotfiles guix guix-configuration guix-home literate-programming
Last synced: 3 months ago
JSON representation
My literate configuration for Guix
- Host: GitHub
- URL: https://github.com/hiecaq/guix-config
- Owner: hiecaq
- License: gpl-3.0
- Created: 2023-01-08T09:07:26.000Z (about 2 years ago)
- Default Branch: main
- Last Pushed: 2024-04-13T11:03:41.000Z (10 months ago)
- Last Synced: 2024-04-14T01:08:00.880Z (10 months ago)
- Topics: dotfiles, guix, guix-configuration, guix-home, literate-programming
- Homepage:
- Size: 846 KB
- Stars: 6
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: readme.org
- License: LICENSE
Awesome Lists containing this project
- awesome-guix - hiecaq/guix-config
README
# -*- org-use-property-inheritance: t; toc-org-max-depth: 4; org-confirm-babel-evaluate: nil; -*-
#+title: hiecaq's guix configuration
#+last_modified: [2024-02-12 Mon 12:45]
#+startup: indent
#+property: header-args :comments org :results silent :mkdirp t#+toc: headlines 2
* Table of Contents :TOC:noexport:
- [[#introduction][Introduction]]
- [[#home-configuration][Home Configuration]]
- [[#guix][Guix]]
- [[#locales][Locales]]
- [[#channels][Channels]]
- [[#cross-desktop-group-xdg][Cross-Desktop Group (XDG)]]
- [[#base-directories][Base Directories]]
- [[#user-directories][User Directories]]
- [[#shells][Shells]]
- [[#fish][Fish]]
- [[#tools][Tools]]
- [[#certificates][Certificates]]
- [[#bat][bat]]
- [[#eza][eza]]
- [[#ripgrep][ripgrep]]
- [[#fd][fd]]
- [[#direnv][Direnv]]
- [[#aliases][Aliases]]
- [[#fonts][Fonts]]
- [[#emacs][Emacs]]
- [[#basics][Basics]]
- [[#early-initialization][Early Initialization]]
- [[#packages][Packages]]
- [[#special-key-remapping][Special Key Remapping]]
- [[#some-configurations-that-might-make-sense-to-put-here][Some Configurations that might make sense to put here]]
- [[#main-configurations][Main Configurations]]
- [[#setupel][setup.el]]
- [[#some-sane-configurations][Some Sane Configurations]]
- [[#window-management][Window Management]]
- [[#universal-argument][Universal Argument]]
- [[#pcre][PCRE]]
- [[#help][Help]]
- [[#xdg][Xdg]]
- [[#no-littering][No Littering]]
- [[#fonts-1][Fonts]]
- [[#modus-themes][Modus Themes]]
- [[#mode-line][Mode Line]]
- [[#midnight][Midnight]]
- [[#auto-save][Auto Save]]
- [[#recentf][Recentf]]
- [[#save-history][Save History]]
- [[#editorconfig][Editorconfig]]
- [[#envrc][Envrc]]
- [[#subword][Subword]]
- [[#highlight-parentheses][Highlight Parentheses]]
- [[#transient][Transient]]
- [[#evil][Evil]]
- [[#evil-surround][Evil Surround]]
- [[#evil-replace-with-register][Evil Replace With Register]]
- [[#evil-snipe][Evil Snipe]]
- [[#evil-commentary][Evil Commentary]]
- [[#window-map][Window map]]
- [[#god-mode][God mode]]
- [[#which-key][Which key]]
- [[#posframe][Posframe]]
- [[#eldoc][Eldoc]]
- [[#ace-window][Ace Window]]
- [[#spell-checking][Spell Checking]]
- [[#flyspell-correct][Flyspell Correct]]
- [[#xref][Xref]]
- [[#topsy][Topsy]]
- [[#orderless][Orderless]]
- [[#vertico][Vertico]]
- [[#marginalia][Marginalia]]
- [[#consult][Consult]]
- [[#embark][Embark]]
- [[#tempel][Tempel]]
- [[#eglot-tempel][Eglot-tempel]]
- [[#corfu][Corfu]]
- [[#visual-undo][Visual Undo]]
- [[#hideshow][Hideshow]]
- [[#pulse][Pulse]]
- [[#electric-pair-mode][electric-pair-mode]]
- [[#aggresive-indent][Aggresive Indent]]
- [[#eshell][Eshell]]
- [[#fish-completion][fish-completion]]
- [[#magit][Magit]]
- [[#project][Project]]
- [[#emacsql][Emacsql]]
- [[#epub][Epub]]
- [[#pdf][Pdf]]
- [[#org-mode][Org Mode]]
- [[#general-settings][General Settings]]
- [[#task-management][Task Management]]
- [[#literate-programming][Literate Programming]]
- [[#evil-org][Evil Org]]
- [[#toc-org][Toc Org]]
- [[#org-appear][Org Appear]]
- [[#personal-knowledge-management][Personal Knowledge Management]]
- [[#style-and-faces][Style and Faces]]
- [[#detached][Detached]]
- [[#english][English]]
- [[#linting][Linting]]
- [[#capitalizing][Capitalizing]]
- [[#eglot][Eglot]]
- [[#haskell][Haskell]]
- [[#rust][Rust]]
- [[#dhall][Dhall]]
- [[#ron][Ron]]
- [[#dart][Dart]]
- [[#plantuml][PlantUML]]
- [[#email][Email]]
- [[#mu4e][mu4e]]
- [[#mpv][MPV]]
- [[#vterm][vterm]]
- [[#eat][Eat]]
- [[#dired][Dired]]
- [[#dired-rsync][dired-rsync]]
- [[#guix-1][Guix]]
- [[#references-and-recommendations][References and Recommendations]]* Introduction
This is my all-in-one [[https://guix.gnu.org/][Guix]] configuration, working in progress. This aims to eventually replace and deprecate [[https://github.com/hiecaq/dotfiles][my dotfiles]], which has too many historical burdens.Unless explicitly stated, all code in this configuration is under GPL3 license.
* Home Configuration
This is the main entry point for =guix home=. It can be tested with
#+begin_src sh
guix home -L build container build/home-configuration.scm
#+end_src
and deployed with
#+begin_src sh
guix home -L build reconfigure build/home-configuration.scm
#+end_src#+begin_src scheme :tangle "build/home-configuration.scm" :noweb yes
(use-modules
(gnu home)
(gnu services)
(gnu packages)
<>
)(home-environment
<>
(services
(append
<>
)))
#+end_srcThis basically reads the default essential service list, and modifies it as needed. ~home-environment-default-essential-services~ is private, so we have to use [[https://www.gnu.org/software/guile/manual/html_node/Using-Guile-Modules.html][@@]] syntax to force importing it. Maybe there is a better way.
#+begin_src scheme :noweb-ref home-environment-conf
(essential-services
(fonts:modify-essential-service
((@@(gnu home) home-environment-default-essential-services)
this-home-environment)))
#+end_srcThis is a list of packages that are not installed by services. Eventually this list should be empty.
#+begin_src scheme :noweb-ref home-environment-conf
(packages (specifications->packages
(list
"neovim"
"guile"
)))
#+end_src* Guix
This file defines those settings related to Guix itself.
#+begin_src scheme :tangle "build/hiecaq/home/guix.scm" :noweb yes
(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
<>
(simple-service
'variant-packages-service
home-channels-service-type
(list
<>
))))
#+end_srcAdd this module and its services:
#+begin_src scheme :noweb-ref home-module
((hiecaq home guix) #:prefix guix:)
#+end_src#+begin_src scheme :noweb-ref home-environment-service
guix:services
#+end_src** Locales
Set the locales as recommended in [[https://guix.gnu.org/manual/en/html_node/Application-Setup.html][the manual]].
#+begin_src scheme :noweb-ref guix-service
(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)))
#+end_src** Channels
:PROPERTIES:
:header-args:scheme: :noweb-ref guix-channel
:END:*** COMMENT RDE
[[https://git.sr.ht/~abcdw/rde][rde]] is a "developers and power user friendly GNU/Linux distribution based on GNU Guix package manager", which can be used as a channel directly. In this way, I can use the helper procedures that it defines.I no longer need functionality belongs to rde, but I keep it here for future reference.
#+begin_src scheme
(channel
(name 'rde)
(url "https://git.sr.ht/~abcdw/rde")
(introduction
(make-channel-introduction
"257cebd587b66e4d865b3537a9a88cccd7107c95"
(openpgp-fingerprint
"2841 9AC6 5038 7440 C7E9 2FFA 2208 D209 58C1 DEB0"))))
#+end_src* Cross-Desktop Group (XDG)
This section defines those settings related to the [[https://www.freedesktop.org/wiki/Specifications/][XDG]] specifications.
#+begin_src scheme :tangle "build/hiecaq/home/xdg.scm" :noweb yes
(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
<>
))
#+end_srcAdd this module and its services:
#+begin_src scheme :noweb-ref home-module
((hiecaq home xdg) #:prefix xdg:)
#+end_src#+begin_src scheme :noweb-ref home-environment-service
xdg:services
#+end_src** Base Directories
See [[https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html][Enviroment Variables chapter in latest XDG Base Directory Specification]] for the description on their purposes.Guix home [[https://git.savannah.gnu.org/cgit/guix.git/tree/gnu/home.scm#n86][instantiate]] [[https://git.savannah.gnu.org/cgit/guix.git/tree/gnu/home/services/xdg.scm#n148][it]] by default, so technically there is no configuration needed, unless we want to modify their values.
Note that their values are set in =$GUIX_HOME/setup-environment=, which should be run by =$HOME/.profile=, which is sourced at the beginning of a login shell.
** User Directories
As declared in [[https://www.freedesktop.org/wiki/Software/xdg-user-dirs/][xdg-user-dirs]], this defines "well known" user directories, and their localization.
#+begin_src scheme :noweb-ref xdg-service
(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")))
#+end_src* Shells
#+begin_src scheme :tangle "build/hiecaq/home/shell.scm"
(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))
#+end_srcTODO: I should split this out later.
#+begin_src scheme :tangle "build/hiecaq/home/shell.scm" :noweb yes
(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")))
<>
))
#+end_srcAdd this module and its services:
#+begin_src scheme :tangle no :noweb-ref home-module
((hiecaq home shell) #:prefix shell:)
#+end_src#+begin_src scheme :tangle no :noweb-ref home-environment-service
shell:services
#+end_src
** Fish
:PROPERTIES:
:header-args:scheme: :noweb-ref shell-service
:END:
I use [[https://fishshell.com/][fish]] as a backup interactive-use-only shell.
#+begin_src scheme
(service
home-fish-service-type)
#+end_src
** COMMENT Zsh
:PROPERTIES:
:header-args:scheme: :noweb-ref shell-service
:END:
I'm currently using [[https://www.zsh.org/][zsh]] as my primary shell.
#+begin_src scheme
(service
home-zsh-service-type
(home-zsh-configuration
(zshrc
(list (slurp-file-like (local-file "../../.zshrc"
"zshrc"))
(slurp-file-like (local-file "../../.aliases"
"aliases"))))))
#+end_src*** syntax highlighting
Add [[https://github.com/zsh-users/zsh-syntax-highlighting][zsh-syntax-highlighting]], which provides "fish shell like syntax highlighting for Zsh."
#+begin_src scheme
(service
(service-type
(name 'home-zsh-syntax-highlighting)
(extensions
(list
(service-extension home-zsh-plugin-manager-service-type
(const
(list
(specification->package
"zsh-syntax-highlighting"))))
(service-extension
home-zsh-service-type
(const
(home-zsh-extension
(zshrc '("# Improve highlighting")))))))
(default-value #f)
(description #f)))
#+end_src
And its configuration
#+begin_src sh :tangle "build/.zshrc"
# Declare the variable
typeset -A ZSH_HIGHLIGHT_STYLES# disable highlighting for unknown-token
ZSH_HIGHLIGHT_STYLES[unknown-token]='none'# use blue to highlight command(e.g., git)
ZSH_HIGHLIGHT_STYLES[command]='fg=004'# builtins(e.g., pwd): blue, italic
ZSH_HIGHLIGHT_STYLES[builtin]='fg=004,standout'# commandseparator(;, &&): lighter gray
ZSH_HIGHLIGHT_STYLES[commandseparator]='fg=014'# alias: blue
ZSH_HIGHLIGHT_STYLES[alias]='fg=004'# single hyphen-option: darker red,italic
ZSH_HIGHLIGHT_STYLES[single-hyphen-option]='fg=001'# double hyphen-option: darker red
ZSH_HIGHLIGHT_STYLES[double-hyphen-option]='fg=001'# quoted arguments(strings)
ZSH_HIGHLIGHT_STYLES[single-quoted-argument]='fg=006'
ZSH_HIGHLIGHT_STYLES[double-quoted-argument]='fg=006'# dollar quoted arguments:gold
ZSH_HIGHLIGHT_STYLES[dollar-quoted-argument]='fg=003'# other commands: red
ZSH_HIGHLIGHT_STYLES[arg0]='fg=001'# To define styles for nested brackets up to level 4
ZSH_HIGHLIGHT_STYLES[bracket-level-1]='fg=010'
ZSH_HIGHLIGHT_STYLES[bracket-level-2]='fg=014'
ZSH_HIGHLIGHT_STYLES[bracket-level-3]='fg=010'
ZSH_HIGHLIGHT_STYLES[bracket-level-4]='fg=014'
ZSH_HIGHLIGHT_STYLES[bracket-error]='fg=001'
ZSH_HIGHLIGHT_STYLES[cursor-matchingbracket]='fg=007'ZSH_HIGHLIGHT_HIGHLIGHTERS=(main brackets)
#+end_src** Tools
:PROPERTIES:
:header-args:scheme: :noweb-ref shell-service
:END:
There are many tools that enhance the command line user experience.
*** Certificates
See the [[https://guix.gnu.org/manual/en/html_node/X_002e509-Certificates.html][Guix documentation]] for details on the CA settings. TODO: Maybe this should be in a higher-level heading?
#+begin_src scheme
(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)))
#+end_src
*** bat
Add [[https://github.com/sharkdp/bat][bat]], which is a =cat= clone with colors.
#+begin_src scheme
(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)))
#+end_src*** eza
[[https://github.com/eza-community/eza][eza]] is a community-revived fork of [[https://github.com/ogham/exa][exa]], which is "a modern replacement for =ls=".
#+begin_src scheme
(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)))
#+end_src*** ripgrep
Add [[https://github.com/BurntSushi/ripgrep][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=.
#+begin_src scheme
(simple-service
'home-ripgrep
home-profile-service-type
(list
(specification->package
"ripgrep")))
#+end_src*** fd
Add [[https://github.com/sharkdp/fd][fd]], which is "a simple, fast and user-friendly alternative to 'find'".
#+begin_src scheme
(simple-service
'home-fd
home-profile-service-type
(list
(specification->package
"fd")))
#+end_src*** Direnv
[[https://direnv.net/][direnv]] is the environment switcher on the shell level, based on current directories.
#+begin_src scheme
(simple-service
'home-direnv
home-profile-service-type
(list
(specification->package
"direnv")))
#+end_src** Aliases
And the aliases that I'm using:
#+begin_src sh :tangle "build/.aliases"
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"
#+end_src* Fonts
:PROPERTIES:
:header-args:scheme: :tangle "build/hiecaq/home/fonts.scm"
:END:This file describe how fonts are configured.
#+begin_src scheme
(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:))
#+end_srcThe ~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.
#+begin_src scheme
(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)))
#+end_srcHere is the modified =fonts.conf=:
#+begin_src nxml :tangle "build/fonts.conf" :comments no
~/.guix-home/profile/share/fonts
serif
Noto Serif
Noto Serif CJK SC
Noto Serif CJK JP
Noto Serif CJK TC
sans-serif
Noto Sans
Noto Sans CJK SC
Noto Sans CJK JP
Noto Sans CJK TC
monospace
Noto Sans Mono
Noto Sans Mono CJK SC
Noto Sans Mono CJK JP
Noto Sans Mono CJK TC
emoji
Noto Color Emoji
#+end_srcthis module simply provides a single service that install the fonts needed.
#+begin_src scheme
(define-public services
(list (simple-service
'extend-environment-variables
home-profile-service-type
(list
font-hack
font-google-noto
font-google-noto-sans-cjk))))
#+end_src#+begin_src scheme :tangle no :noweb-ref home-module
((hiecaq home fonts) #:prefix fonts:)
#+end_src#+begin_src scheme :tangle no :noweb-ref home-environment-service
fonts:services
#+end_src* Emacs
:PROPERTIES:
:header-args:emacs-lisp: :lexical t :tangle "build/init.el"
:header-args:lisp-data: :tangle "build/templates.eld"
:header-args:scheme: :noweb-ref emacs-service
:END:
TODO: I'm still not sure if I should put some config as big as Emacs' in this file.
#+begin_src scheme :tangle "build/hiecaq/home/emacs.scm" :noweb yes :noweb-ref nil
(define-module (hiecaq home emacs)
#:use-module (gnu services)
#:use-module (gnu packages)
#:use-module (gnu home services)
#:use-module (gnu home services shells)
#:use-module (guix packages)
#:use-module (guix gexp))(define-public services
(list
<>))
#+end_srcAdd this module and its services:
#+begin_src scheme :noweb-ref home-module
((hiecaq home emacs) #:prefix emacs:)
#+end_src#+begin_src scheme :noweb-ref home-environment-service
emacs:services
#+end_src
** Basics
I'm currently using =emacs= from Guix official channel.
#+begin_src scheme
(service
(service-type
(name 'home-emacs)
(extensions
(list
(service-extension
home-profile-service-type
(const (list
(specification->package
"emacs"))))
(service-extension
home-xdg-configuration-files-service-type
(const `(("emacs/init.el" ,(local-file "../../init.el"))
("emacs/early-init.el" ,(local-file "../../early-init.el"))
("emacs/config/templates.eld" ,(local-file "../../templates.eld")))))
(service-extension
home-environment-variables-service-type
(const '(("EDITOR" . "emacsclient -a nvim -c")
("VISUAL" . "emacsclient -a nvim -c"))))))(default-value #f)
(description #f)))
#+end_srcI and my Guix packages definition is at =(hiecaq packages emacs-xyz)=. TODO: makes a channel!
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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:))
#+end_srcNOTE: the hash for git-based packages is got by following [[https://guix.gnu.org/cookbook/en/html_node/Extended-example.html][Guix Cookbook instructions]].
** Early Initialization
:PROPERTIES:
:header-args:emacs-lisp: :lexical t :tangle "build/early-init.el"
:END:
#+begin_src emacs-lisp :comments no
;;; early-init.el --- Configurations before package systems and UI systems -*- lexical-binding: t; buffer-read-only: t; eval: (auto-revert-mode 1) -*-
#+end_src
*** Packages
I don't use the built-in =package.el= to fetch packages, so I'll turn it off:
#+begin_src emacs-lisp
(setq package-enable-at-startup nil)
#+end_src*** Special Key Remapping
grabbed from [[https://emacsnotes.wordpress.com/2022/09/11/three-bonus-keys-c-i-c-m-and-c-for-your-gui-emacs-all-with-zero-headache/][Three bonus keys—‘C-i’, ‘C-m’ and ‘C-[’—for your GUI Emacs; all with zero headache]]
#+begin_src emacs-lisp
(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])))))
#+end_src*** Some Configurations that might make sense to put here
~load~ prefers the newest version of a file (when suffix is not given).
#+begin_src emacs-lisp
(setq load-prefer-newer t)
#+end_src#+begin_src emacs-lisp
(setq load-no-native t)
#+end_src** Main Configurations
Init file header:
#+begin_src emacs-lisp :comments no
;;; init.el --- Main Configurations -*- lexical-binding: t; buffer-read-only: t; eval: (auto-revert-mode 1) -*-
#+end_srcUse Utf-8 as the default coding system.
#+begin_src emacs-lisp
(set-language-environment "UTF-8")
(prefer-coding-system 'utf-8-unix)
#+end_src
*** setup.el
[[https://www.emacswiki.org/emacs/SetupEl][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 [[https://github.com/jwiegley/use-package][use-package]].
#+begin_src scheme
(simple-service
'home-emacs-setup
home-profile-service-type
(list
(specification->package
"emacs-setup")))
#+end_srcSee Alternative Macro Definer at [[https://www.emacswiki.org/emacs/SetupEl][its Emacs Wiki page]], and [[https://github.com/mfiano/emacs-config/blob/main/lisp/mf-setup.el][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.
#+begin_src emacs-lisp
(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 " <%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))
#+end_src*** Some Sane Configurations
#+begin_src emacs-lisp
(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))
#+end_srcTurn 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.
#+begin_src emacs-lisp
(setup emacs
(:option create-lockfiles nil))
#+end_src4-space indentation:
#+begin_src emacs-lisp
(setup simple
(:option tab-width 4))
#+end_srcGeneral programming set up:
#+begin_src emacs-lisp
(setup prog-mode
(:hook #'display-line-numbers-mode)
(:local-set truncate-lines t))
#+end_srcWhen 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:make-backup-files][help]] for details.
#+begin_src emacs-lisp
(setup files
(:option make-backup-files nil)
(:option backup-by-copying t))
#+end_srcAlways use =y-or-p= over =yes-or-no=, and use ~read-key~ instead of ~read-from-minibuffer~. The latter is helpful when using Embark.
#+begin_src emacs-lisp
(setup emacs
(:option use-short-answers t
y-or-n-p-use-read-key t))
#+end_srcI don't want Emacs to auto-recenter when scrolling off-the-screen:
#+begin_src emacs-lisp
(setup emacs
(:option scroll-conservatively 101))
#+end_srcOne extra thing: Emacs comes with a customization interface, which supports setting via function calls too (good!) and saves the results in a file (bad!). This snippet set the storage to =/dev/null=:
#+begin_src emacs-lisp
(setup cus-edit
(:option custom-file null-device))
#+end_src*** Window Management
#+begin_src emacs-lisp
(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))))
#+end_src#+begin_src emacs-lisp
(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)))))
#+end_src#+begin_src emacs-lisp
(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"))))))
#+end_src*** Universal Argument
I am using [[https://www.kaufmann.no/roland/dvorak/][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.
#+begin_src emacs-lisp
(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 "" (lambda (arg)
(interactive "P")
(insert (format "%s" arg))))))
#+end_srcAlso here is a helper macro for binding commands. I personally do not like using universal argument at all.
#+begin_src emacs-lisp
(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))))
#+end_src*** PCRE
Emacs comes with an [[info:elisp#Rx Notation][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. [[https://github.com/joddie/pcre2el][pcre2el]] is the missing bridge between PCRE, Emacs regex string and rx notation.
#+begin_src scheme
(simple-service
'home-emacs-setup
home-profile-service-type
(list
(specification->package
"emacs-pcre2el")))
#+end_src
*** Help
TODO: this should not require help.
#+begin_src emacs-lisp
(setup (:require help)
(:global (:unbind "C-h C-h")))
#+end_src*** Xdg
I add a ~xdg-log-home~ getter definition. This is technically not part of the standard, but Guix has this environment variable defined, so why not?
#+begin_src emacs-lisp
(setup (:require xdg)
(unless (functionp #'xdg-log-home)
(defun xdg-log-home ()
"Return the base directory for user-specific log data."
(xdg--dir-home "XDG_LOG_HOME" "~/.local/log"))))
#+end_src*** No Littering
[[https://github.com/emacscollective/no-littering][no-littering]] helps put emacs directory clean, sorting package-created files and directories into reasonable directories. One thing it misses is the distinguishing between permanent data and temporary data. I used to fork it to provide this distinguishing, but it turns out to be too troublesome to maintain. Now I simply consider this as a "fallback" solution. Later on for the variables from packages I really use I'll overwrite them manually.
#+begin_src scheme
(simple-service
'home-emacs-setup
home-profile-service-type
(list
(specification->package
"emacs-no-littering")))
#+end_src#+begin_src emacs-lisp
(setup (:require no-littering))
#+end_src#+begin_src emacs-lisp
(defmacro def-exdg-home-dir (xdg-name)
(list 'progn
`(defvar ,(intern (format "exdg-%s-dir" xdg-name))
(expand-file-name (convert-standard-filename "emacs/") (,(intern (format "xdg-%s-home" xdg-name)))))
`(defun ,(intern (format "exdg-%s" xdg-name)) (file)
(expand-file-name (convert-standard-filename file) ,(intern (format "exdg-%s-dir" xdg-name))))))(def-exdg-home-dir config)
(def-exdg-home-dir cache)
(def-exdg-home-dir data)
(def-exdg-home-dir state)
(def-exdg-home-dir log)(setq exdg-config-dir (expand-file-name "config/" user-emacs-directory))
#+end_src*** Fonts
#+begin_src emacs-lisp
(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))
#+end_src*** Modus Themes
#+begin_src scheme
(simple-service
'home-emacs-modus-themes
home-profile-service-type
(list
(specification->package
"emacs-modus-themes")))
#+end_src#+begin_src emacs-lisp
(setup modus-themes
(:option modus-themes-mixed-fonts t)
(:require modus-themes)
(load-theme 'modus-vivendi :no-confirm))
#+end_src*** Mode Line
#+begin_src emacs-lisp
(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)
#+end_src*** Midnight
=midnight= is Emacs' built-in cron-like service that run once during midnight each day. Its main purpose is to do same maintenance for the Emacs instance, such as cleaning very old unused buffers. It simply invokes ~midnight-hook~ (which contains ~#'clean-buffer-list~ by default) ~midnight-delay~ seconds after the midnight.#+begin_src emacs-lisp
(setup midnight
(:option midnight-delay (* 4 60 60))
(:enable))
#+end_src
*** Auto Save
#+begin_src emacs-lisp
(setup files
(let ((autosave-dir (exdg-cache "auto-save/")))
(mkdir autosave-dir t)
(:option auto-save-file-name-transforms
`(("\\`/[^/]*\\([^/]*/\\)*\\([^/]*\\)\\'" ,(concat autosave-dir "\\2") t)))))
#+end_src
*** Recentf
[[info:emacs#File Conveniences][recentf]] is an Emacs built-in minor mode that saves recent file list.
#+begin_src emacs-lisp
(setup recentf
(:option recentf-save-file (exdg-state "recentf-save.el"))
(:enable))
#+end_src*** Save History
[[help:savehist-mode][savehist]] is an Emacs built-in minor mode that save minibuffer histories to a file.
#+begin_src emacs-lisp
(setup savehist
(:option savehist-file (exdg-state "savehist.el"))
(:enable))
#+end_src*** COMMENT Save Place
[[https://www.emacswiki.org/emacs/SavePlace][Save Place]] is a Emacs built-in mode that "nave place in files between sessions".
#+begin_src emacs-lisp
(setup saveplace
(:option save-place-forget-unreadable-files nil)
(save-place-mode 1))
#+end_src*** Editorconfig
[[https://editorconfig.org/][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.[[https://github.com/editorconfig/editorconfig-emacs][editorconfig-emacs]] implements its own =editorconfig= core, so it's logical to assume that it works on any platform.
#+begin_src scheme
(simple-service
'home-emacs-editorconfig
home-profile-service-type
(list
(specification->package
"emacs-editorconfig")))
#+end_src#+begin_src emacs-lisp
(setup editorconfig
(:enable))
#+end_src*** Envrc
[[https://github.com/purcell/envrc][envrc]] is Emacs' integration with [[https://direnv.net/][direnv]] that works in buffer-local style.[[https://github.com/purcell/inheritenv][interitenv]].
#+begin_src scheme
(simple-service
'home-emacs-envrc
home-profile-service-type
(list
(specification->package
"emacs-envrc")
(specification->package
"emacs-inheritenv")))
#+end_src#+begin_src emacs-lisp
(setup envrc
(:also-load inheritenv)
(:with-mode envrc-global-mode
(:hook-into after-init)))
#+end_src*** Subword
[[help:subword-mode][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 [[help:superword-mode][superword-mode]] BTW. See [[info:emacs#MixedCase Words][MixedCase Words]] and [[info:emacs#Misc for Programs][Misc for Programs]] in the documentation.
#+begin_src emacs-lisp
(setup subword
(:hook-into text-mode prog-mode))
#+end_src*** Highlight Parentheses
[[https://git.sr.ht/~tsdh/highlight-parentheses.el][highlight-parentheses]], well, highlights parentheses surrounding [[info:emacs#Point][point]].
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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))))
#+end_src#+begin_src scheme
(simple-service
'home-emacs-highlight-parentheses
home-profile-service-type
(list
(specification->package
"emacs-highlight-parentheses")))
#+end_srcThe configs here is basically from [[https://protesilaos.com/emacs/modus-themes#h:24bab397-dcb2-421d-aa6e-ec5bd622b913][Note on highlight-parentheses.el]] in Modus Themes documentation, modified a little bit.
#+begin_src emacs-lisp
(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)))
#+end_src*** Transient
#+begin_src emacs-lisp
(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")))
#+end_src*** Evil
It's name tells everything: the Extensible Vi Layer for Emacs, [[https://github.com/emacs-evil/evil][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:
- [[https://github.com/noctuid/evil-guide][evil-guide]] by noctuid
#+begin_src scheme
(simple-service
'home-emacs-evil
home-profile-service-type
(specifications->packages
(list
"emacs-goto-chg"
"emacs-evil"
"emacs-evil-collection-next"
"emacs-evil-surround"
"emacs-evil-snipe"
"emacs-evil-commentary")))
#+end_srcI need some latest contributions to the =evil-collection= repository:
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(define-public emacs-evil-collection-next
(let ((commit "5886bab852dc9e31959e70384d535473e44504ad")
(last-release-version "0.0.10")
(revision "0"))
(package
(inherit upstream:emacs-evil-collection)
(name "emacs-evil-collection-next")
(version (git-version last-release-version revision commit))
(source
(origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/emacs-evil/evil-collection")
(commit commit)))
(file-name (git-file-name name version))
(sha256
(base32
"0dz9dkmxm4j2r2nilgxwgvsgbm531rrsiszzx480zrmqybdsziq6")))))))
#+end_src#+begin_src emacs-lisp
(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 == 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 "" #'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))
#+end_src**** Evil Surround
[[https://github.com/emacs-evil/evil-surround][evil-surround]] defines operators that change/add/delete delimiters around a text object.
I found that its key bindings conflict with =evil-snipe= a lot, so I remap them to =m=, which stands for markers.
#+begin_src emacs-lisp
(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)))
#+end_src**** Evil Replace With Register
[[https://github.com/Dewdrops/evil-ReplaceWithRegister][evil-replace-with-register]] defines a =replace= operator. However, we can implement its functionality easily with Evil mode itself, see [[https://emacs-china.org/t/evil-replace-with-register/27638][this post]]. I add some simple code to the solution there to make =""= register work as the way I want.
#+begin_src emacs-lisp
(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 "")
(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))))
#+end_src**** Evil Snipe
[[https://github.com/hlissner/evil-snipe][evil-snipe]] is a Evil port of Vim's [[https://github.com/rhysd/clever-f.vim][clever-f]] and [[https://github.com/justinmk/vim-sneak][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.
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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))))
#+end_src#+begin_src emacs-lisp
(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)))
#+end_src**** Evil Commentary
[[https://github.com/linktohack/evil-commentary][evil-commentary]] defines operators for commenting.
#+begin_src emacs-lisp
(setup evil-commentary
(:also-load evil)
(:enable))
#+end_src**** Window map
Add my helper commands to the ~evil-window-map~
#+begin_src emacs-lisp
(setup evil
(:with-map evil-window-map
(:bind "M-s" #'my-window-shot
"M-r" #'my-window-record)))
#+end_src*** God mode
[[https://github.com/emacsorphanage/god-mode][god-mode]] provides a minor mode in which modifier keys of key bindings are handled sepecially: =C-= is not needed any more, =M-= is implied with a single key, etc.
#+begin_src scheme
(simple-service
'home-emacs-setup
home-profile-service-type
(list
(specification->package
"emacs-god-mode")))
#+end_src#+begin_src emacs-lisp
(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-" #'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))
#+end_src*** Which key
[[https://github.com/justbur/emacs-which-key][which-key]] is a minor mode that hints you the keybindings prefixed with what you have typed when you get stuck.
#+begin_src scheme
(simple-service
'home-emacs-setup
home-profile-service-type
(list
(specification->package
"emacs-which-key")))
#+end_src#+begin_src emacs-lisp
(setup (:require which-key)
(:option which-key-show-transient-maps t
which-key-use-C-h-commands nil)
(which-key-enable-god-mode-support)
(:enable))
#+end_srcAs a side note, which-key default configuration requires there to be at least 1 slot at the bottom in ~window-sides-slots~.
*** Posframe
[[https://github.com/tumashu/posframe][posframe]] pops a child-frame at point, connected to its root window's buffer.
#+begin_src scheme
(simple-service
'home-emacs-setup
home-profile-service-type
(list
(specification->package
"emacs-posframe")))
#+end_src*** Eldoc
#+begin_src emacs-lisp
(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))))))
#+end_src[[https://github.com/casouri/eldoc-box][eldoc-box]] shows eldoc in a separate childframe instead of the crowded echo area.
#+begin_src scheme
(simple-service
'home-emacs-eldoc-box
home-profile-service-type
(list
(specification->package
"emacs-eldoc-box")))
#+end_src#+begin_src emacs-lisp
(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))
#+end_src*** Ace Window
[[https://github.com/abo-abo/ace-window][ace-window]] is helpful to do things the "embark" way: pick a window, then decide what to do with it.Its package definition in the Guix official channel is for the "latest" release version, which is as old as 2014. So I makes a variation to use the master branch HEAD at the time of writing.
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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")))))))
#+end_src#+begin_src scheme
(simple-service
'home-emacs-ace-window
home-profile-service-type
(list
(specification->package
"emacs-ace-window-next")))
#+end_src#+begin_src emacs-lisp
(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)))
#+end_src=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.
#+begin_src emacs-lisp
(setup ace-window-posframe
(:enable))
#+end_src*** Spell Checking
See the [[info:emacs#Spelling][documentation]] for details.Emacs comes with a spell checking wrapper...
#+begin_src emacs-lisp
(setup ispell
(:needs "hunspell")
(:option ispell-program-name "hunspell"))
#+end_src... and an on-the-fly spell checker(which uses ~ispell~ as the backend).
#+begin_src emacs-lisp
(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)))
#+end_src**** Flyspell Correct
The default UI for ~ispell~ is quite hard to use, and there is a package [[https://github.com/d12frosted/flyspell-correct][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:
#+begin_src scheme
(simple-service
'home-emacs-flyspell-correct
home-profile-service-type
(list
(specification->package
"hunspell")
(specification->package
"hunspell-dict-en-us")
(specification->package
"emacs-flyspell-correct-next")))
#+end_srcI drop the unused dependencies. It is ridiculous to have to propagate =ivy=, =helm= and =popup= to use this package.
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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")))))))
#+end_src#+begin_src emacs-lisp
(setup (:require flyspell-correct)
(:needs "hunspell")
(:also-load flyspell)
(:global (:rebind #'ispell-word #'flyspell-correct-wrapper)))
#+end_src*** Xref
=xref= is an Emacs built-in cross referencing browsing package.
#+begin_quote
This file provides a somewhat generic infrastructure for cross referencing commands, in particular "find-definition".
#+end_quote#+begin_src emacs-lisp
(setup xref
(:option xref-search-program 'ripgrep)
(:global (:with-state (normal)
(:bind "g r" #'xref-find-references))))
#+end_src*** Topsy
[[https://github.com/alphapapa/topsy.el][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.
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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))))
#+end_srcAlthough =topsy= recommends to use =org-sticky-header= instead, this snippet for org-mode is good enough for me:
#+begin_src emacs-lisp
(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))
#+end_src#+begin_src scheme
(simple-service
'home-emacs-topsy
home-profile-service-type
(list
(specification->package
"emacs-topsy")))
#+end_src*** Orderless
[[https://github.com/oantolin/orderless][orderless]] add space-separated component (which then matches against several matching styles) completion style to minibuffer and other completion UI.
#+begin_src scheme
(simple-service
'home-emacs-orderless
home-profile-service-type
(list
(specification->package
"emacs-orderless")))
#+end_srcOrderless needs [[https://github.com/minad/consult/wiki#orderless-style-dispatchers-ensure-that-the--regexp-works-with-consult-buffer][some hack]] to work with ~consult-buffer~ and friends. Steal from [[https://github.com/minad/consult/wiki#minads-orderless-configuration][minad's]]:
#+begin_src emacs-lisp
(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)))))))
#+end_srcSometimes it can be useful to use rx-notation directly.
#+begin_src emacs-lisp
(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)))
#+end_srcFor 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.
#+begin_src emacs-lisp
(setup orderless
(:option orderless-style-dispatchers '(+orderless-consult-dispatch
orderless-kwd-dispatch
orderless-affix-dispatch)
orderless-matching-styles '(orderless-regexp)))
#+end_srcAffix dispatcher can be adjust by setting the ~orderless-affix-dispatch-alist~, which maps the single affix character to matcher.
#+begin_src emacs-lisp
(setup orderless
(with-eval-after-load 'orderless
(:option (prepend orderless-affix-dispatch-alist) `(?_ . ,#'my-orderless-rx)
(prepend orderless-affix-dispatch-alist) `(?- . ,#'orderless-prefixes))))
#+end_srcNote that =file= no longer needs special treat for recent Emacs and Tramp, see [[https://github.com/minad/vertico?tab=readme-ov-file#tramp-hostname-and-username-completion][here]].
Finally, define how the completion system actually works. Minad states in the above notes that
#+begin_quote
Note that ~completion-category-overrides~ is not really an override, but rather prepended to the default ~completion-styles~.
#+end_quote#+begin_src emacs-lisp
(setup minibuffer
(:option completion-category-defaults nil)
(:option completion-styles '(orderless basic)
completion-category-overrides '((file (styles partial-completion)))))
#+end_srcWe can also defines our own completion style as used in ~completion-styles~ etc, with the help of orderless.
#+begin_src emacs-lisp
(setup orderless
(with-eval-after-load 'orderless
(orderless-define-completion-style orderless-only-initialism
(orderless-matching-styles '(orderless-initialism)))))
#+end_srcMy 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~.
#+begin_src emacs-lisp
(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))
#+end_src*** Vertico
[[https://github.com/minad/vertico][vertico]] "provides a performant and minimalistic vertical completion UI based on the default completion system."
#+begin_src scheme
(simple-service
'home-emacs-setup
home-profile-service-type
(list
(specification->package
"emacs-vertico")))
#+end_srcBy 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~:
#+begin_src emacs-lisp
(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)))
#+end_src#+begin_src emacs-lisp
(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))
#+end_src*** Marginalia
[[https://github.com/minad/marginalia][marginalia]] adds info to the right of completion candidates, thus the name margin-alia.
#+begin_src scheme
(simple-service
'home-emacs-setup
home-profile-service-type
(list
(specification->package
"emacs-marginalia")))
#+end_src#+begin_src emacs-lisp
(setup (:require marginalia)
(:enable))
#+end_src*** Consult
[[https://github.com/minad/consult][consult]] provides practical commands based on the Emacs completion function =completing-read=. What this means is that basically =consult= pop up candidates when calling its commands into =comleting-read=.
#+begin_src scheme
(simple-service
'home-emacs-setup
home-profile-service-type
(list
(specification->package
"emacs-consult")))
#+end_src#+begin_src emacs-lisp
(setup (:require consult)
(:option consult-preview-key "C-j"
xref-show-definitions-function #'consult-xref
xref-show-xrefs-function #'consult-xref
consult-locate-args "locate --ignore-case --regex")
;; from https://github.com/minad/consult/wiki#consult-ripgrep-or-line-counsel-grep-or-swiper-equivalent
(defcustom my-consult-ripgrep-or-line-limit 300000
"Buffer size threshold for `my-consult-ripgrep-or-line'.
When the number of characters in a buffer exceeds this threshold,
`consult-ripgrep' will be used instead of `consult-line'."
:type 'integer)(defun my-consult-ripgrep-or-line ()
"Call `consult-line' for small buffers or `consult-ripgrep' for large files."
(interactive)
(if (or (not buffer-file-name)
(buffer-narrowed-p)
(ignore-errors
(file-remote-p buffer-file-name))
(jka-compr-get-compression-info buffer-file-name)
(<= (buffer-size)
(/ my-consult-ripgrep-or-line-limit
(if (eq major-mode 'org-mode) 2 1))))
(consult-line)
(when (file-writable-p buffer-file-name)
(save-buffer))
(let ((consult-ripgrep-args
(concat consult-ripgrep-args
" --hidden")))
(consult-ripgrep (list buffer-file-name)))))(defmacro my-consult-with-no-sep (fn)
(let* ((fn-value (eval fn))
(old-name (symbol-name fn-value))
(new-name (concat old-name "-with-no-sep"))
(doc (documentation fn-value)))
`(progn (defun ,(intern new-name) ()
,doc
(interactive)
(require 'orderless)
(let ((completion-styles '(orderless))
(completion-category-defaults nil)
(completion-category-overrides nil)
(orderless-component-separator 'list))
(call-interactively ,fn))
#',(intern new-name)))));; from https://github.com/minad/consult/issues/318#issuecomment-882067919
;; with some tweaks
(defun my-consult-line-evil-history (&rest _)
"Add latest `consult-line' search pattern to the evil search history ring.
This only works with orderless and interprets the whole string as a single
component."
(when-let ((_ (bound-and-true-p evil-mode))
(_ (eq evil-search-module 'evil-search))
(hist (car consult--line-history))
(orderless-component-separator 'list)
(pattern (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))))
#+end_srcFor ~consult-grep~ families and ~consult-find~ families, it is possible to convert orderless patterns into their PCRE pattern inputs, as suggested by the [[https://github.com/minad/consult/wiki#use-orderless-as-pattern-compiler-for-consult-grepripgrepfind][Wiki]].
#+begin_src emacs-lisp
(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))
#+end_src~consult-info~ can be used as a ~Info-search~ drop-in replacement:
#+begin_src emacs-lisp
(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")))
#+end_src*** Embark
[[https://github.com/oantolin/embark][embark]] is probably the most world-changing package in Emacs recently. It basically provides a just-in-time context-aware action list (quite like no-repeating hydra or which-key) in minibuffer on the =complete-read= candidate or on anything in the editing file.Reference:
- [[https://github.com/oantolin/embark/wiki][wiki]]
- [[https://karthinks.com/software/fifteen-ways-to-use-embark/][15 ways to use embark]]#+begin_src scheme
(simple-service
'home-emacs-embark
home-profile-service-type
(list
(specification->package
"emacs-embark")))
#+end_src#+begin_src emacs-lisp
(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))))))
#+end_src#+begin_src emacs-lisp
(setup (:require embark-consult))
#+end_srcI 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.
#+begin_src emacs-lisp
(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))
#+end_src
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:
#+begin_src emacs-lisp
(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)))
#+end_src#+begin_src emacs-lisp
(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)
#+end_src#+begin_src emacs-lisp
(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))
#+end_src#+begin_src emacs-lisp
(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))
#+end_srcWith ~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 [[https://github.com/minad/vertico/wiki#automatically-shrink-vertico-for-embark-live][the following solution]]. Note that this needs ~vertico-multiform-mode~.
#+begin_src emacs-lisp
(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)))
#+end_srcI found that very often I want the buffer opened by embark to be somewhere I assign. Adapted from [[https://karthinks.com/software/fifteen-ways-to-use-embark/][Karthik Chikmagalur's hack]] and [[https://karthinks.com/software/emacs-window-management-almanac/#a-window-prefix-command-for-ace-window][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.
#+begin_src emacs-lisp
(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)))
#+end_src
Note: Somehow only post-hooks can recognize ~(minibufferp)~.*** Tempel
[[https://github.com/minad/tempel][tempel]] is a "tiny template package for Emacs", using the built-in template package Tempo's syntax. I use it instead of famous [[https://github.com/joaotavora/yasnippet][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.
#+begin_src scheme
(simple-service
'home-emacs-tempel
home-profile-service-type
(list
(specification->package "emacs-tempel")
(specification->package "emacs-eglot-tempel-next")))
#+end_srcThe functions here come from tempel's README.
#+begin_src emacs-lisp
(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)))
#+end_srcglobal templates
#+begin_src lisp-data
fundamental-mode(date (format-time-string "%Y-%m-%d"))
#+end_src**** Eglot-tempel
Tempel itself, unlike YASnippet, does not support LSP snippet expansion out of the box.
This feature is notably useful when you auto-complete a function name, in which case the argument list is the snippet.Anyway, [[https://github.com/fejfighter/eglot-tempel][eglot-tempel]], as the name suggests, bridges eglot's snippet interface with tempel. There is also [[https://github.com/svaante/lsp-snippet][lsp-snippet]] that might worth checking later.
The version in Guix official channel is as old as 2022, so again I declare a HEAD variation.
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(define-public emacs-eglot-tempel-next
(let ((commit "303c7c24e140121f8bc218249b6169d0471b77b8")
(last-release-version "0.5.0")
(revision "0"))
(package
(inherit upstream:emacs-eglot-tempel)
(name "emacs-eglot-tempel-next")
(version (git-version last-release-version revision commit))
(source
(origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/fejfighter/eglot-tempel")
(commit commit)))
(file-name (git-file-name name version))
(sha256
(base32
"10scmnkvp2aid9a4bb26cvg8vag6plkrnpg96dylwm0g6rra19zp"))))
(arguments
(list #:tests? #false))
(native-inputs (list)))))
#+end_src#+begin_src emacs-lisp
(setup eglot-tempel
(:hook-into eglot-server-initialized-hook))
#+end_src
*** Corfu
[[https://github.com/minad/corfu][corfu]] is a ~completion-at-point~ implementation that is much more concise than =company=.
#+begin_src scheme
(simple-service
'home-emacs-setup
home-profile-service-type
(list
(specification->package
"emacs-corfu")))
#+end_src#+begin_src emacs-lisp
(setup corfu
(:option corfu-preview-current nil
corfu-quit-at-boundary nil)
(:option tab-always-indent 'complete)
(:with-state (insert emacs)
(:global (:bind "" #'completion-at-point)) ;; see early-init.el
(:with-map corfu-map (:bind "" #'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)))
#+end_src
*** Visual Undo
[[https://github.com/casouri/vundo][vundo]] is basically a less-buggy [[https://www.dr-qubit.org/undo-tree.html][undo-tree]] that supports Emacs 28's new ~undo-redo~.
#+begin_src scheme
(simple-service
'home-emacs-vundo
home-profile-service-type
(list
(specification->package
"emacs-vundo")))
#+end_src#+begin_src emacs-lisp
(setup (:require vundo)
(:with-map my-leader-map (:bind "u" #'vundo)))
#+end_src*** Hideshow
[[info:emacs#Hideshow][hideshow]] is Emacs' built-in code folding package.
#+begin_src emacs-lisp
(setup hideshow
(:with-mode hs-minor-mode (:hook-into prog-mode)))
#+end_src*** Pulse
=pulse= is a built-in package that transiently highlights a region (current cursor line, for example). Its callbacks can be added to post jump hooks, so that the jumps are easier to follow.
#+begin_src emacs-lisp
(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)))
#+end_src*** electric-pair-mode
=electric-pair-mode= is a built-in package that auto insert the left bracket/parentheses when we type the left one. It also skip the right bracket/parentheses if we type it. This behavior might be familiar to many IDE users.
#+begin_src emacs-lisp
(setup elec-pair
(:with-mode electric-pair-local-mode
(:hook-into prog-mode minibuffer-setup)))
#+end_src*** Aggresive Indent
[[https://github.com/Malabarba/aggressive-indent-mode][aggressize-indent-mode]] basically reindents what you have changed after every change you made.
#+begin_src scheme
(simple-service
'home-emacs-setup
home-profile-service-type
(list
(specification->package
"emacs-aggressive-indent")))
#+end_src#+begin_src emacs-lisp
(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))
#+end_src*** Eshell
I plan on switching to =eshell= as my main shell. Here are some references:
- [[https://howardism.org/Technical/Emacs/eshell-why.html][Why Use Eshell?]] by Howard Abrams
- [[https://web.archive.org/web/20201111230155/https://ambrevar.xyz/emacs-eshell/][Eshell as a main shell (web archived)]] by Pierre Neidhardt
- [[https://famme.sk/blog/how-about-eshell.html][BASH, ZSH, FISH. How about Eshell?]] from TRITON FAMME
- [[https://emacs-china.org/t/topic/5362][Discussion on Tweaking Eshell]] on Emacs China (in Chinese)
- [[https://www.masteringemacs.org/article/complete-guide-mastering-eshell][Mastering Eshell]] by Mickey Petersen
- [[https://github.com/condy0919/emacs-newbie/blob/master/introduction-to-builtin-modes.md#eshell][Introductions to Emacs Builtin Mode Features]] from =emacs-newbie= (in Chinese)
- [[https://emacs.stackexchange.com/questions/75369/use-hs-minor-mode-where-its-not-supported][Use =hs-minor-mode= Where It's Not Supported]] asked on Emacs Stack Exchange
- [[https://blog.liangzan.net/blog/2012/12/12/customizing-your-emacs-eshell-prompt/][Customizing Your Emacs Eshell Prompt]] by Liang Zan
- [[https://www.emacswiki.org/emacs/CategoryEshell][Eshell Category]] on Emacs Wiki
- [[https://www.gnu.org/software/emacs/manual/html_mono/eshell.html][Eshell's Offical Manual]]#+begin_src emacs-lisp
(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))))
#+end_src**** fish-completion
[[https://gitlab.com/Ambrevar/emacs-fish-completion][fish-completion]] is a cool package that empowers Eshell with auto-completion feature from the fish shell. This package even has the ability to fallback on auto-completion provided by bash shell, although I'm not using that right now.
#+begin_src scheme
(simple-service
'home-emacs-fish-completion
home-profile-service-type
(list
(specification->package
"emacs-fish-completion")))
#+end_src#+begin_src emacs-lisp
(setup fish-completion
(:needs "fish")
(:hook-into eshell-mode))
#+end_src*** Magit
[[https://github.com/magit/magit][magit]] is an Emacs interface to git, which provides not only commands to call but also a full GUI-like wrapper around git.
#+begin_src scheme
(simple-service
'home-emacs-magit
home-profile-service-type
(list
(specification->package
"emacs-magit")))
#+end_src#+begin_src emacs-lisp
(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))
#+end_srcIts Evil integration is now a part of evil-collection.
*** Project
Since Emacs 28, the built-in =project.el= implements most functionalities needed for project management, which makes [[https://github.com/bbatsov/projectile][projectile]] unnecessary.
#+begin_src emacs-lisp
(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))))
#+end_src#+begin_src emacs-lisp
(defun embark-target-project ()
(cons 'project
(if-let ((project (project-current nil))
(project-name (project-name project)))
project-name
"")))(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)
#+end_src*** Emacsql
[[https://github.com/magit/emacsql][emacsql]] is "a high-level Emacs Lisp RDBMS front-end", which provides a consistent facade for different sqlite integration implementations. There is one tagged version in Guix package upstream, but it is too old for my need (and it comes with too many unnecessary dependencies), see below.
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(define-public emacs-emacsql-minimal
(package
(inherit upstream:emacs-emacsql)
(name "emacs-emacsql-minimal")
(inputs (list upstream:emacs-minimal))
(propagated-inputs (list))
(build-system emacs-build-system)
(arguments
'(#:include '("emacsql.el" "emacsql-compiler.el" "emacsql-sqlite-common.el")))))
#+end_src=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.
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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")))))
#+end_src#+begin_src scheme
(simple-service
'home-emacs-emacsql
home-profile-service-type
(list
(specification->package
"emacs-emacsql-sqlite-builtin")))
#+end_src*** Epub
Emacs' built-in [[https://www.gnu.org/software/emacs/manual/html_node/emacs/Document-View.html][doc-view-mode]] is said to support Epub format, but I've never got it to work. [[https://depp.brause.cc/nov.el/][nov.el]] to the rescue.#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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")))))))
#+end_src#+begin_src scheme
(simple-service
'home-emacs-nov-el
home-profile-service-type
(list
(specification->package
"emacs-nov-el-next")))
#+end_src#+begin_src emacs-lisp
(setup nov
(:option nov-save-place-file (exdg-state "nov-save-place.el")
(prepend auto-mode-alist) `(,(rx ".epub" eos) . nov-mode)))
#+end_src
For PDF files, Emacs' built-in [[info:emacs#Document View][doc-view]] mode is actually quite usable. It pre-renders the PDF files into images and save them in the filesystem.Anyway, I use [[https://github.com/vedang/pdf-tools][pdf-tools]] which relies on an external program =epdfinfo= that utilizes [[https://poppler.freedesktop.org/][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.
#+begin_src scheme
(simple-service
'home-emacs-pdf-tools
home-profile-service-type
(list
(specification->package
"emacs-pdf-tools")))
#+end_srcIts Guix package already handles the =pdfinfo= program, so I set it up without letting it re-attempt the build-on-the-fly process.
#+begin_src emacs-lisp
(setup pdf-tools
(pdf-loader-install nil t))
#+end_src*** Org Mode
From its website
#+begin_quote
Org mode is for keeping notes, maintaining TODO lists, planning projects, and authoring documents with a fast and effective plain-text system.
#+end_quote
this is only a facial overall summary of what [[https://orgmode.org][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:
- [[https://alphapapa.github.io/org-almanac/][org-almanac]], an "awesome"-ish list of what people are using Org Mode for.#+begin_src scheme
(simple-service
'home-emacs-org
home-profile-service-type
(specifications->packages
(list "emacs-org"
"emacs-evil-org"
"emacs-toc-org"
"emacs-org-appear-next"
"emacs-org-download-next")))
#+end_src#+begin_src emacs-lisp :noweb yes
(setup org
<>
(:hook visual-line-mode variable-pitch-mode))
#+end_src**** General Settings
Turn on =org-indent=, aka clean view by default:
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-startup-indented t)
#+end_srcEnforce to-do dependencies (i.e. children block their parent)
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-enforce-todo-dependencies t)
#+end_srcWhen 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.
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-special-ctrl-a/e t)
(:option org-special-ctrl-k t)
#+end_srcPrevent =M-RET= from splitting the line if the line is a headline or an item.
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-M-RET-may-split-line '((default . nil)))
#+end_srcUpdate =#+last_modified= every time an org file is saved.
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(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)
#+end_src#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-persist-directory (exdg-cache "org-persist"))
#+end_srcOrg-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.
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-startup-folded 'showall)
#+end_srcDisable 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~.
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-src-window-setup 'plain)
#+end_srcTemplates:
#+begin_src lisp-data
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))
#+end_src
**** Task Management
I generally follow the GTD way as my task management system.***** Tasks and Logs
Todo state keywords. The todo state is simple:
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-todo-keywords
'((sequence "TODO(t!)" "NEXT(e!)" "WAIT(w@/@)" "|" "DONE(d@)")
("|" "CANCELED(c@)")
("|" "MEETING(m)")
("|" "PHONE(p)")))
#+end_srcLog into a =LOGBOOK= drawer so that things are folded when we want to read about outcome descriptions
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-log-into-drawer t)
#+end_srcWhen refiling, log down a timestamp:
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-log-refile t)
#+end_srcI 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.
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-log-done 'note)
#+end_srcPut newer note at the top:
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-reverse-note-order t)
#+end_src***** Effort Measurement and Time Cost Estimates
Org mode provides the feature to estimate effort and track time spent on a task.First, if something somehow has a =0:00= duration, don't count it.
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-clock-out-remove-zero-time-clocks t)
#+end_srcClock out when a task is =DONE= or =CANNCELED=
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-clock-out-when-done t)
#+end_srcSometimes, 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.
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-clock-persist t
org-clock-persist-file (exdg-state "org-clock-persist.el"))
#+end_src#+begin_src emacs-lisp
(with-eval-after-load 'org
(org-clock-persistence-insinuate))
#+end_src**** Literate Programming
References:
+ [[https://orgmode.org/worg/org-contrib/babel/intro.html][Babel: Introduction]] in worg/org-contrib
+ [[https://orgmode.org/manual/Working-with-Source-Code.html#Working-with-Source-Code][Working with Source Code]] from =org-mode='s manual
+ [[https://howardism.org/Technical/Emacs/literate-programming-tutorial.html][Introduction to Literate Programming]] by Howard Abrams**** Evil Org
[[https://github.com/Somelauw/evil-org-mode][evil-org]] is org mode's evil integration. It provides not simply keybindings, but also text objects.
#+begin_src emacs-lisp
(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))))
#+end_src**** Toc Org
[[https://github.com/snosov1/toc-org][toc-org]] will automatically update the content of the first heading with a =:TOC:= tag in an org file to show an up-to-date TOC whenever the file is saved. Handy!
#+begin_src emacs-lisp
(setup toc-org
(:also-load org)
(:hook-into org-mode))
#+end_src**** Org Appear
[[https://github.com/awth13/org-appear][org-appear]] is a minor mode that "toggle visibility of hidden Org mode element parts upon entering and leaving an element", and it works with Evil very well.Its package definition in the Guix Official Channel is for the "latest" release version, which is as old as January 2022. So I makes a variation to use the master branch HEAD at the time of writing.
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(define-public emacs-org-appear-next
(let ((commit "81eba5d7a5b74cdb1bad091d85667e836f16b997")
(last-release-version "0.3.0")
(revision "0"))
(package
(inherit upstream:emacs-org-appear)
(name "emacs-org-appear-next")
(version (git-version last-release-version revision commit))
(source
(origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/awth13/org-appear")
(commit commit)))
(file-name (git-file-name name version))
(sha256
(base32
"1jh2rdp7rx1hnsfky5di1amz8rc5jf0qlc5ykr09m5f9fpz9m9x6")))))))
#+end_src#+begin_src emacs-lisp
(setup org-appear
(:option org-appear-trigger 'manual
org-appear-autolinks t
org-appear-autosubmarkers t
org-appear-autoentities t)
(defun my-org-appear-setup ()
(cond (org-appear-mode
(add-hook 'evil-normal-state-exit-hook #'org-appear-manual-start nil t)
(add-hook 'evil-normal-state-entry-hook #'org-appear-manual-stop nil t))
(t
(remove-hook 'evil-normal-state-exit-hook #'org-appear-manual-start t)
(remove-hook 'evil-normal-state-entry-hook #'org-appear-manual-stop t))))
(:hook-into org-mode)
(:hook #'my-org-appear-setup))
#+end_src**** Personal Knowledge Management
I believe strongly that [[https://en.wikipedia.org/wiki/Personal_information_management][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 [[https://en.wikipedia.org/wiki/Zettelkasten][Zettelkasten]].
***** Org Id
Enable tracking org heading links using globally unique UIDs. This is a must-have even without =org-roam=, because org mode won't fix the broken links when you refile/archive some subtrees to a different file.
#+begin_src emacs-lisp
(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")))
#+end_src***** Custom Links
Org-mode has built-in manpage link support, but it is not on by default:
#+begin_src emacs-lisp
(setup (:require ol-man))
#+end_src=lfile= looks up the link by querying =plocate= database, which is a pre-indexed DB for local files.
Based on [[https://karl-voit.at/2022/02/10/lfile/][blog post]] from [[https://github.com/novoid][Karl Voit]].
#+begin_src emacs-lisp
(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"
))
#+end_srcHere [[https://org-roam.discourse.group/t/implementing-hierarchies-namespaces-in-org-roam-v2/1504][is]] [[https://org-roam.discourse.group/t/link-categorization/2486][an]] [[https://org-roam.discourse.group/t/custom-roam-style-link/39/57][attempt]] [[https://forum.zettelkasten.de/discussion/887/thinking-about-metadata][to]] [[https://forum.zettelkasten.de/discussion/573/idea-for-categorizing-theory-models-definitions-arguments-and-facts][implement]] [[https://org-roam.discourse.group/t/add-link-tags-feature/][Link]] [[https://org-roam.discourse.group/t/the-case-for-custom-link-types/][Tags]].
#+begin_src emacs-lisp
(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)))
#+end_src***** Org Roam
[[https://www.orgroam.com/][org-roam]] basically [[https://blog.jethro.dev/posts/org_roam_v2/][does]] [[https://www.orgroam.com/manual.html#Org_002droam_0027s-Design-Principle][2 things]]:
1. Use a sqlite database to cache everything that is getting slow as notes scaling up
2. Using this database to display "backlinks" for a note, a fancy word standing for the links that point to the current note.
This means that, giving that org-roam is quite stable now, we can use the database to do [[https://github.com/org-roam/org-roam/wiki/User-contributed-Tricks][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.
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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))))))))))
#+end_srcAnd a missing gem [[https://github.com/ahmed-shariff/org-roam-ql/][org-roam-ql]], which has a query syntax and feature set similar to [[https://github.com/alphapapa/org-ql][org-ql]] (It starts as a "spin-off" from org-ql I think, see [[https://github.com/alphapapa/org-ql/issues/303][this]] and [[https://github.com/alphapapa/org-ql/issues/354][this]]). Basically it turns a s-exp query into a series of SQL queries to the org-roam database.
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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+))))
#+end_src#+begin_src scheme
(simple-service
'home-emacs-org-roam
home-profile-service-type
(specifications->packages
(list
"emacs-org-roam-next"
"emacs-org-roam-ql")))
#+end_src#+begin_src emacs-lisp
(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))))
#+end_srcAdd consult source:
#+begin_src emacs-lisp
(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)))
#+end_src#+begin_src emacs-lisp
(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)))
#+end_srcSide 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.
#+begin_src emacs-lisp
(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))))))
#+end_src***** Bibliography
#+begin_src emacs-lisp
(defvar my-global-bibliography
(list (expand-file-name "notes/refs.bib" (xdg-user-dir "DOCUMENTS")))
"A list to global bib files.")
#+end_src****** ebib
[[https://github.com/joostkremers/ebib][Ebib]] is technically not related to org-mode in most aspects. It is a front-end to BibTeX/BibLaTeX files.
#+begin_src scheme
(simple-service
'home-emacs-ebib
home-profile-service-type
(specifications->packages
(list
"emacs-ebib")))
#+end_src#+begin_src emacs-lisp
(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)))
#+end_src
****** citar
There are quite a lot bibliographic packages, among which I use [[https://github.com/emacs-citar/citar][citar]].
#+begin_src scheme
(simple-service
'home-emacs-citar
home-profile-service-type
(specifications->packages
(list
"emacs-citar-next"
"emacs-citar-org-roam-next")))
#+end_src#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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")))))))
#+end_src#+begin_src emacs-lisp
(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))
#+end_srcIt 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.
#+begin_src emacs-lisp
(setup citar-embark
(:option citar-at-point-function #'embark-act)
(:enable))
#+end_src=citar= comes with its own =org-roam= integration as a [[https://github.com/emacs-citar/citar-org-roam][separate package]]:
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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))))
#+end_srcAs far as I can tell, ~citar-org-roam-mode~ automatically sets up ~citar-notes-sources~, making the related configuration unnecessary.
#+begin_src emacs-lisp
(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))
#+end_srcOne great feature of =citar-org-roam= is that [[https://github.com/org-roam/org-roam/issues/2207][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
[[https://github.com/abo-abo/org-download][org-download]], despite its name, is an all-in-one image insertion solution for org-mode. It saves the image, no matter where it is from, online or in clipboard, and then inserts the link into org-mode.
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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")))))))
#+end_srcI mainly use it to keep images referenced in org-roam notes. There is no other place where I need referencing images anyway!
#+begin_src emacs-lisp
(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"))
#+end_src**** Style and Faces
This part of code is basically grabbed from [[https://zzamboni.org/post/beautifying-org-mode-in-emacs/][Beautifying Org Mode in Emacs]] by zzamboni.Hide ===, =~= and other emphasis markers, and fontify src block natively:
#+begin_src emacs-lisp :tangle no :noweb-ref org-setup
(:option org-hide-emphasis-markers t
org-use-sub-superscripts '{}
org-src-fontify-natively t
org-tags-column 0)
#+end_src*** Detached
[[https://sr.ht/~niklaseklund/detached.el/][detached]] utilizes [[https://github.com/crigler/dtach][dtach]] and provides Emacs-integrated features. It can do many cool things, see [[https://emacsconf.org/2022/talks/detached/][EmacsConf2022 Talk]] for its demonstration.
#+begin_src scheme
(simple-service
'home-emacs-detached
home-profile-service-type
(list
(specification->package
"emacs-detached")))
#+end_src#+begin_src emacs-lisp
(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))
#+end_srcIts consult integration is currently broken on my site, so let's fix it:
#+begin_src emacs-lisp
(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))))))))
#+end_src*** English
**** Linting
#+begin_src scheme
(simple-service
'home-emacs-flymake-vale
home-profile-service-type
(list
(specification->package
"emacs-flymake-vale")))
#+end_src#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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)))))
#+end_src#+begin_src emacs-lisp
(setup flymake-vale
(:with-function flymake-vale-load
(:hook-into text-mode)))
#+end_src
**** Capitalizing
[[https://github.com/duckwork/titlecase.el][titlecase]] solves one of the hardest problem in (English) writing: capitalizing titles. its most impressing feature is that it supports many standard styles, like [[https://www.chicagomanualofstyle.org/book/ed17/part2/ch08/toc.html][Chicago]] and [[https://blog.apastyle.org/apastyle/capitalization/][APA]]. I mainly use it with embark.
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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))))
#+end_src#+begin_src scheme
(simple-service
'home-emacs-titlecase
home-profile-service-type
(list
(specification->package
"emacs-titlecase")))
#+end_src#+begin_src emacs-lisp
(setup (:require titlecase)
(:also-load embark)
(:with-map embark-heading-map
(:bind "T" #'titlecase-line))
(:with-map embark-region-map
(:bind "T" #'titlecase-region)))
#+end_src*** Eglot
Eglot is Emacs' built-in LSP client.
#+begin_src emacs-lisp
(setup eglot
(with-eval-after-load 'eglot
(set-face-attribute 'eglot-highlight-symbol-face nil :inherit 'highlight)))
#+end_src*** Haskell
#+begin_src scheme
(simple-service
'home-emacs-haskell
home-profile-service-type
(list
(specification->package
"emacs-haskell-mode")))
#+end_src*** Rust
#+begin_src scheme
(simple-service
'home-emacs-rust
home-profile-service-type
(list
(specification->package
"emacs-eglot")
(specification->package
"emacs-rustic-minimal")))
#+end_srcIts package definition introduces =lsp-mode= and =flycheck=, so I defines a variant to drop them.
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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))))))#+end_src
#+begin_src emacs-lisp
(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)))
#+end_src*** Dhall
#+begin_src scheme
(simple-service
'home-emacs-dhall
home-profile-service-type
(list
(specification->package
"dhall")
(specification->package
"emacs-dhall-mode-next")))
#+end_src#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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")))))))
#+end_src*** Ron
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(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))))
#+end_src#+begin_src scheme
(simple-service
'home-emacs-ron-mode
home-profile-service-type
(list
(specification->package
"emacs-ron-mode")))
#+end_src*** Dart
#+begin_src scheme
(simple-service
'home-emacs-dart
home-profile-service-type
(list
(specification->package
"emacs-dart-mode-minimal")))
#+end_srcIt's weird that it introduces many unnecessary propagated inputs, so I make a variant to drop them.
#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(define-public emacs-dart-mode-minimal
(package
(inherit upstream:emacs-dart-mode)
(name "emacs-dart-mode-minimal")
(propagated-inputs (list))))
#+end_src*** PlantUML
#+begin_src scheme
(simple-service
'home-emacs-plantuml
home-profile-service-type
(list
(specification->package
"emacs-plantuml-mode")))
#+end_src#+begin_src emacs-lisp
(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))))
#+end_src
**** mu4e
#+begin_src scheme
(service
(service-type
(name 'home-mu)
(extensions
(list
(service-extension
home-profile-service-type
(const (list
(specification->package
"isync")
(specification->package
"rss2email")
(specification->package
"msmtp")
(specification->package
"mu"))))
(service-extension
home-environment-variables-service-type
(const '(("XAPIAN_CJK_NGRAM" . "1"))))))
(default-value #f)
(description #f)))
#+end_src
***** Back-end Initialization
Here is How =mu= is initialized. This needs to be run manually.
#+begin_src sh :noweb yes
mu init --maildir=<> '--my-address=/<>/'
#+end_src#+name: mu-maildir
#+begin_src elisp :exports none
(expand-file-name "mails" (xdg-user-dir "DOCUMENTS"))
#+end_src#+name: mu-my-address
#+begin_src elisp :exports none
(rxt-elisp-to-pcre (rx (: (+ (any alnum ".")) "@" "hiecaq" "." "org")))
#+end_src
***** Front-end Settings
#+begin_src emacs-lisp
(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))))
#+end_src
*** MPV
[[https://github.com/kljohann/mpv.el][mpv.el]] controls [[https://mpv.io/][mpv]] via its IPC interface, useful for note-taking.I don't want to use directly Guix upstream's =emacs-mpv=, which depends on its packaged =mpv=, because I currently just use Arch's directly. So let's make a variant.
#+begin_src scheme
(service
(service-type
(name 'home-emacs-mpv)
(extensions
(list
(service-extension
home-profile-service-type
(const (list
(specification->package
"emacs-mpv-minimal"))))))
(default-value #f)
(description #f)))
#+end_src#+begin_src scheme :tangle "build/hiecaq/packages/emacs-xyz.scm" :noweb-ref nil
(define-public emacs-mpv-minimal
(package
(inherit upstream:emacs-mpv)
(name "emacs-mpv-minimal")
(inputs (modify-inputs (package-inputs upstream:emacs-mpv)
(delete "mpv")))
(arguments
(append
(substitute-keyword-arguments
(package-arguments upstream:emacs-mpv)
((#:phases phases)
`(modify-phases ,phases
(delete 'patch-exec-paths))))))))
#+end_srcAdvice ~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~)
#+begin_src emacs-lisp
(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))))
#+end_srcAdd ~mpv-seek-to-position-at-point~ to ~org-open-at-point-functions~ on demand:
#+begin_src emacs-lisp
(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)))))
#+end_srcDefine an embark target and keymap, which can be used when mpv is running.
#+begin_src emacs-lisp
(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))))
#+end_src
*** vterm
#+begin_src scheme
(simple-service
'home-emacs-vterm
home-profile-service-type
(list
(specification->package
"emacs-vterm")))
#+end_src#+begin_src scheme
(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")))))
#+end_src#+begin_src emacs-lisp
(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))))))
#+end_src*** Eat
[[https://codeberg.org/akib/emacs-eat][eat]] is a terminal emulator for Emacs, similar to =vterm=, but implemented fully in Elisp.
#+begin_src scheme
(simple-service
'home-emacs-eat
home-profile-service-type
(list
(specification->package
"emacs-eat")))
#+end_src#+begin_src emacs-lisp
(setup eat
(:option eshell-visual-commands nil
eshell-visual-subcommands nil
eshell-visual-options nil)
(eat-eshell-mode +1))
#+end_src*** Dired
=dired= is Emacs' built-in file explorer. It has a classic text-based UI that is so easy to use that many community-maintained packages follow its design principles.**** dired-rsync
One thing that dired (with tramp) does it badly is copying files over network. For small files it is fine, for big files not only is it slow but also it blocks the whole Emacs while copying. [[https://github.com/stsquad/dired-rsync][dired-rsync]] to the rescue, which basically wraps [[https://rsync.samba.org/][rsync]] and does things asynchronously.
#+begin_src scheme
(simple-service
'home-emacs-dired-rsync
home-profile-service-type
(list
(specification->package
"emacs-dired-rsync")))
#+end_src#+begin_src emacs-lisp
(setup dired
(:hook #'dired-hide-details-mode)
(:option dired-dwim-target t))
#+end_src#+begin_src emacs-lisp
(setup dired-rsync
(:option dired-rsync-options "-azs --info=progress2"))
#+end_src*** Guix
#+begin_src scheme
(simple-service
'home-emacs-guix
home-profile-service-type
(list
(specification->package
"emacs-guix")))
#+end_src* References and Recommendations
This configuration is written while referencing the following guix configurations:
- [[https://git.sr.ht/~akagi/guixrc][Aleksandr Vityazev's Guix Configuration]]
- [[https://github.com/dustinlyons/guix-config][Dustin Lyon's Literate Configuration for Guix Linux]]
- [[https://git.envs.net/iyzsong/guixrc.git][iyzsong's Guix System and Home Config]]
- [[https://git.sr.ht/~krevedkokun/dotfiles][Nikita Domnitskii's Dotfiles]]
- [[https://github.com/nicolas-graves/dotfiles][Nicolas Graves's Dotfiles]]
- [[https://github.com/qbladea/linux-os][Luhux 的 Guix 操作系统配置文件]]
- [[https://git.sr.ht/~abcdw/rde][rde by Andrew Tropin]]. I use it as a channel.
- [[https://github.com/tumashu/geeguix][Tumashu's Guixsd Configuration]]
- [[https://codeberg.org/hako/Rosenthal/][Rosenthal by Hako]], a Guix channel
- [[https://github.com/yveszoundi/guix-config/][Yves Zoundi's Guix Configuration]]