Merrick’s Doom Configuration
About
This is my personal Doom Emacs configuration, serving as the central nervous system for my workstation, Lagann. This setup is deeply integrated with the GNU Guix ecosystem, leveraging its package management to ensure stable, reproducible native dependencies (like liberime and pdf-tools).
The configuration is built around three core pillars:
- Knowledge Management: Transitioning from a fragmented setup to a centralized, Zettelkasten-inspired system using Denote. This also serves as the engine for my personal blog.
- Project Isolation: Utilizing
tabspaces(backed by the built-intab-bar-mode) to manage context-specific workspaces and projects without the overhead of heavy-handed workspace managers. - Native Performance: Prioritizing Guix-managed packages and built-in Emacs features where possible to maintain a lean, high-performance environment.
Doom Modules
This block is copied from the doom emacs init.example.el, and then modified based on my needed modules, used to enable/disable doom modules.
;;; init.el -*- lexical-binding: t; -*-
(doom! :input
;;bidi ; (tfel ot) thgir etirw uoy gnipleh
(chinese ; wo shuru buliao zhongwen le
+rime ; best input method (at least for chinese?)
+childframe) ; more like a input method
;;japanese
;;layout ; auie,ctsrnm is the superior home row
:completion
;;(company ; the ultimate code completion backend
;; +childframe) ; ... when your children are better than you
(corfu +icons +orderless) ; complete with cap(f), cape and a flying feather!
;;helm ; the *other* search engine for love and life
;;ido ; the other *other* search engine...
;; (ivy ; a search engine for love and life
;; +fuzzy ; ... icons are nice
;; +prescient) ; ... I know what I want(ed)
(vertico +childframe) ; the search engine of the future
:ui
;;deft ; notational velocity for Emacs
doom ; what makes DOOM look the way it does
dashboard ; a nifty splash screen for Emacs
doom-quit ; DOOM quit-message prompts when you quit Emacs
;;(emoji +unicode) ; 🙂
hl-todo ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW
;;hydra
indent-guides ; highlighted indent columns
ligatures ; ligatures and symbols to make your code pretty again
;;minimap ; show a map of the code on the side
modeline ; snazzy, Atom-inspired modeline, plus API
nav-flash ; blink the current line after jumping
;;neotree ; a project drawer, like NERDTree for vim
ophints ; highlight the region an operation acts on
(popup ; tame sudden yet inevitable temporary windows
+all ; catch all popups that start with an asterix
+defaults) ; default popup rules
;;smooth-scroll ; So smooth you won't believe it's not butter
;;tabs ; an tab bar for Emacs
treemacs ; a project drawer, like neotree but cooler
;;unicode ; extended unicode support for various languages
;;(vc-gutter +pretty) ; vcs diff in the fringe
vi-tilde-fringe ; fringe tildes to mark beyond EOB
;;window-select ; visually switch windows
;;workspaces ; tab emulation, persistence & separate workspaces
zen ; distraction-free coding or writing
:editor
(evil +everywhere) ; come to the dark side, we have cookies
file-templates ; auto-snippets for empty files
fold ; (nigh) universal code folding
(format +onsave) ; automated prettiness
;;god ; run Emacs commands without modifier keys
;;lispy ; vim for lisp, for people who don't like vim
multiple-cursors ; editing in many places at once
;;objed ; text object editing for the innocent
;;parinfer ; turn lisp into python, sort of
rotate-text ; cycle region at point between text candidates
snippets ; my elves. They type so I don't have to
(whitespace +guess +trim) ; a butler for your whitespace
;;word-wrap ; soft wrapping with language-aware indent
:emacs
dired ; making dired pretty [functional]
electric ; smarter, keyword-based electric-indent
;;eww ; the internet is gross
ibuffer ; interactive buffer management
tramp ; remote files at your arthritic fingertips
undo ; persistent, smarter undo for your inevitable mistakes
vc ; version-control and Emacs, sitting in a tree
:term
eshell ; a consistent, cross-platform shell (WIP)
;;shell ; a terminal REPL for Emacs
;;term ; terminals in Emacs
;;vterm ; another terminals in Emacs
ghostel ; the best terminal emulation in Emacs
:checkers
(syntax +flymake) ; tasing you for every semicolon you forget
grammar ; tasing grammar mistake every you make
;;(spell ; tasing you for misspelling mispelling
;; +aspell)
:tools
;; ansible
;;biblio ; Writes a PhD for you (citation needed)
;;collab ; buffers with friends
;;debugger ; FIXME stepping through code, to help you add bugs
direnv
docker
;;editorconfig ; let someone else argue about tabs vs spaces
;;ein ; tame Jupyter notebooks with emacs
(eval +overlay) ; run code, run (also, repls)
(lookup ; helps you navigate your code and documentation
+docsets) ; ...or in Dash docsets locally
(lsp +eglot +booster) ; M-x vscode
llm ; when I said you needed friends, I didn't mean...
magit ; a git porcelain for Emacs
;;+forge
make ; run make tasks from Emacs
pass ; password manager for nerds
pdf ; pdf enhancements
;;prodigy ; FIXME managing external services & code builders
;;rgb ; creating color strings
;;taskrunner ; taskrunner for all your projects
;;terraform ; infrastructure as code
;;tmux ; an API for interacting with tmux
tree-sitter ; syntax and parsing, sitting in a tree...
;;upload ; map local to remote projects via ssh/ftp
:os
(:if IS-MAC macos) ; improve compatibility with macOS
;;tty ; improve the terminal Emacs experience
:lang
;;ada ; In strong typing we (blindly) trust
;;agda ; types of types of types of types...
;;assembly ; assembly for fun or debugging
(beancount +lsp) ; mind the GAAP
(cc +lsp +tree-sitter) ; C/C++/Obj-C madness
clojure ; java with a lisp
common-lisp ; if you've seen one lisp, you've seen them all
;;coq ; proofs-as-programs
;;crystal ; ruby at the speed of c
;;csharp ; unity, .NET, and mono shenanigans
data ; config/data formats
;;(dart +flutter) ; paint ui and not much else
;;dhall
(elixir +lsp +tree-sitter) ; erlang done right
;;elm ; care for a cup of TEA?
emacs-lisp ; drown in parentheses
;;(erlang +lsp) ; an elegant language for a more civilized age
;;ess ; emacs speaks statistics
;;factor
;;faust ; dsp, but you get to keep your soul
;;fortran ; in FORTRAN, GOD is REAL (unless declared INTEGER)
;;fsharp ; ML stands for Microsoft's Language
;;fstar ; (dependent) types and (monadic) effects and Z3
(gdscript +lsp) ; the language you waited for
(go +lsp +tree-sitter) ; the hipster dialect
;;(graphql +lsp) ; Give queries a REST
;;(haskell +lsp) ; a language that's lazier than I am
;;hy ; readability of scheme w/ speed of python
;;idris ; a language you can depend on
;;json ; At least it ain't XML
;;janet ; Fun fact: Janet is me!
;;(java +lsp) ; the poster child for carpal tunnel syndrome
(javascript +lsp +tree-sitter) ; all(hope(abandon(ye(who(enter(here))))))
;;julia ; a better, faster MATLAB
;;(kotlin +lsp) ; a better, slicker Java(Script)
;;latex ; writing papers in Emacs has never been so fun
;;lean
;;ledger ; be audit you can be
(lua +fennel) ; one-based indices? one-based indices
markdown ; writing docs for people to ignore
;;nim ; python + lisp at the speed of c
nix ; I hereby declare "nix geht mehr!"
;;(ocaml +lsp) ; an objective camel
;;odin ; C, minus its footguns
(org ; organize your plain life in plain text
+dragndrop ; drag & drop files/images into org buffers
;;+hugo ; use Emacs for hugo blogging
+roam2
;;+journal
+pretty
;;+noter
;;+jupyter ; ipython/jupyter support for babel
;;+pandoc ; export-with-pandoc support
+pomodoro ; be fruitful with the tomato technique
+present) ; using org-mode for presentations
;;php ; perl's insecure younger brother
plantuml ; diagrams for confusing people more
;;graphviz ; diagrams for confusing yourself even more
;;purescript ; javascript, but functional
(python ; beautiful is better than ugly
+lsp
+tree-sitter
+poetry
+pyright)
;;qt ; the 'cutest' gui framework ever
;;(racket +lsp) ; a DSL for DSLs
;;raku ; the artist formerly known as perl6
(rest +jq) ; Emacs as a REST client
;;rst ; ReST in peace
(ruby +rails) ; 1.step {|i| p "Ruby is #{i.even? ? 'love' : 'life'}"}
(rust +lsp +tree-sitter) ; Fe2O3.unwrap().unwrap().unwrap().unwrap()
;;scad ; trust the preview, regret the render
;;scala ; java, but good
(scheme +guile +mit) ; a fully conniving family of lisps
;;+guile) ; guix!
(sh +fish +lsp) ; she sells {ba,z,fi}sh shells on the C xor
;;sml
;;solidity ; do you need a blockchain? No.
;;swift ; who asked for emoji variables?
;;terra ; Earth and Moon in alignment for performance.
(web ; the tubes
+html +css +lsp)
yaml
(zig +lsp) ; C, but simpler
;; tailwindcss
;; mermaid
:email
;;mu4e
notmuch
;;(wanderlust +gmail)
:app
;;calendar ; A dated approach to timetabling
;;emms ; Multimedia in Emacs is music to my ears
everywhere ; *leave* Emacs!? You must be joking.
irc ; how neckbeards socialize
(rss +org +youtube) ; emacs as an RSS reader
;;twitter ; twitter client https://twitter.com/vnought
;;transmission
:config
literate ; use home made literate solution
(default +bindings +smartparens))
Preamble
Header
Make this file run (slightly) faster with lexical binding (see this blog post for more info).
;;; config.el -*- lexical-binding: t; -*-
Personal Info
It’s getting personal here.
(setq user-full-name "Merrick Luo"
user-mail-address "merrick@luois.me")
Custom Packages
Some package customization become quite extensive, so I place them in separate files and add the lisp directory to the load path.
(add-to-list 'load-path (expand-file-name "lisp" doom-private-dir))
Appearance
Frame Opacity
I like the frame to be a little bit transparent, so I can see my carefully selected wallpapers a little bit.
I had to disable this for now due to the poor performance under wayland + pgtk.
(set-frame-parameter (selected-frame) 'alpha '(95 . 90))
(add-to-list 'default-frame-alist '(alpha . (95 . 90)))
Font
As coming to fonts, I’m not a affectionate person, I switch them around from time to time, and always looking for new ones too, currently I’m happy with Aporetic Serif Mono.
(setq doom-font "Aporetic Sans Mono-13"
doom-variable-pitch-font "Aporetic Sans")
;;doom-serif-font "IBM Plex Serif"
;;doom-unicode-font "LXGW Wenkai")
org-modern relies on some symbols JetBrains Mono doesn’t have, use Iosevka as recommended.
(after! org-modern
(setq org-modern-table nil) ;; conflict with valign
(set-face-attribute 'org-modern-symbol nil :family "Iosevka"))
The line-spacing Hack
Line spacing makes text more readable to me, but some buffer doesn’t really work too well with it.
(setq-default line-spacing 4)
(defun m/unset-line-spacing()
(setq-local line-spacing nil))
(add-hook! '(+doom-dashboard-mode-hook ghostel-mode-hook) #'m/unset-line-spacing)
This is now disabled as well, it doesn’t work as I expected, so i currently just disable line spacing for the terminal.
(after! vterm
(define-advice window-body-height (:around (orig-fn &optional win pixelwise) +vterm--window-body-height-a)
"Include line-spacing in the height calculation of the vterm buffer."
(if (not (provided-mode-derived-p major-mode 'vterm-mode))
(funcall orig-fn win pixelwise)
(let ((pixel-height (funcall orig-fn win t))
(char-height (funcall orig-fn win))
(line-height (frame-char-height (window-frame win)))
(line-spacing-1 (or (and line-spacing (* line-spacing 2)) 0)))
(if pixelwise
(- pixel-height (* char-height line-spacing-1))
(round (/ pixel-height (+ line-height line-spacing-1))))))))
Theme
I rely on modus-themes (specifically modus-operandi) for its industry-leading accessibility and high-contrast light design. Since dark mode causes eye strain during long sessions, these themes are essential for my daily comfort and productivity.
(package! modus-themes)
(setq doom-theme 'modus-operandi)
(use-package! modus-themes
:demand t
:init
(setq modus-themes-italic-constructs t
modus-themes-bold-constructs t
modus-themes-variable-pitch-ui nil
modus-themes-org-blocks 'gray-background)
;; I customize the mode-line background to make it slightly more distinct
;; while staying within the modus-operandi aesthetic.
(custom-set-variables
'(modus-operandi-palette-overrides
'((bg-mode-line-active "#d0d6ff"))))
(setq modus-themes-headings
(quote ((1 . (variable-pitch 1.05))
(agenda-date . (1.1))
(agenda-structure . (variable-pitch light 1.5))
(t . (variable-pitch 1)))))
:config
(modus-themes-load-theme 'modus-operandi))
Misc
I use evil with relative line numbers, but not really utilizing it, cuz I just press j so hard :).
(setq display-line-numbers-type 'relative)
use column style for highlight-indent-guides.
(after! highlight-indent-guides
(setq highlight-indent-guides-method 'column))
Modeline
Some minor customization for doom-modeline.
(after! doom-modeline
(setq doom-modeline-unicode-fallback nil
doom-modeline-height nil
doom-modeline-persp-name t
doom-modeline-persp-icon nil
doom-modeline-hud t))
Spacious Padding
Having some padding on frame looks pretty nice, especially when you’re using emacs as terminal.
(package! spacious-padding)
(use-package! spacious-padding
:custom (spacious-padding-widths '(:internal-border-width 16
:header-line-width 4
:mode-line-width 1
:tab-width 4
:right-divider-width 16
:scroll-bar-width 8))
:config
(spacious-padding-mode))
Better Defaults
Various global settings I collected from the internet.
(after! emacs
(setq-default delete-by-moving-to-trash t
window-combination-resize t
x-stretch-cursor t)
(setq undo-limit 80000000
evil-want-fine-undo t
auto-save-default t
truncate-string-ellipsis "…"
password-cache-expiry nil
scroll-margin 2
pixel-scroll-precision-mode t
elisp-fontify-semantically t)
(global-subword-mode 1)
(xref-mouse-mode 1)
(map! :leader
(:prefix "w"
:desc "Flip layout" "f" #'flip-window-layout
:desc "Rotate layout" "R" #'rotate-window-layout)))
Completion
After using line-spacing, 17 items in completion is too much, reduce it to 12.
(after! vertico
(setq vertico-count 12))
(after! corfu
(setq +corfu-want-minibuffer-completion nil)
(setq corfu-preselect 'first))
EVIL
Please don’t quit, EVIL.
;; :q should kill the current buffer rather than quitting emacs entirely
(evil-ex-define-cmd "q" 'kill-this-buffer)
(evil-ex-define-cmd "wq" 'doom/save-and-kill-buffer)
Spell Checking
Jinx is a better/faster spell checker, plus spell-fu doesn’t work for me anyway, prefer built-in package here because of package with native dependencies works better when installed via Guix.
(package! jinx :built-in 'prefer)
(use-package! jinx
:hook (emacs-startup . global-jinx-mode)
:config
(add-to-list 'jinx-exclude-regexps '(t "\\cc")))
(map! :n "z =" #'jinx-correct)
Mac OS
Not my favorite OS, but I have to use it for work.
(when IS-MAC
(add-to-list 'load-path "/opt/local/share/emacs/site-lisp/mu4e")
(setq mac-command-modifier 'meta
mac-option-modifier 'super)
(global-unset-key [swipe-left])
(global-unset-key [swipe-right]))
Org Mode
While I’ve migrated most of my knowledge management to Denote, Org Mode remains the foundation for my tasks, agenda, and complex document authoring.
(after! org
(setq org-directory "~/syncthing/docs"))
Better defaults
These settings refine the Org experience, focusing on visual clarity (like hiding emphasis markers) and a clean, minimalist interface.
(after! org
(setq org-ellipsis " …"
org-auto-align-tags nil
org-tags-column 0
org-hide-emphasis-markers t))
Appearance
(after! org
(add-hook! 'org-mode-hook #'mixed-pitch-mode)
(setq org-startup-indented nil)
(remove-hook! 'text-mode-hook #'display-line-numbers-mode))
Web Clip
(package! org-web-tools)
(use-package! org-web-tools :defer t)
Super Agenda
I’m using org-super-agenda to make my agenda a little bit more beautiful.
(package! org-super-agenda)
(use-package! org-super-agenda
:after org-agenda
:config
(org-super-agenda-mode)
(setq org-super-agenda-groups
'((:name "In Progress" :todo "STRT")
(:name "Today" :time-grid t :scheduled today :todo "STRT")
(:name "Important" :priority "A")
(:name "Overdue" :deadline past)
(:name "Due Soon" :deadline future))))
Logging
(after! org
(setq org-log-into-drawer t)
;; todo faces for reading list
(add-to-list 'org-todo-keyword-faces
'(("PAUS" . +org-todo-onhold))))
Screenshot
org-download is included in doom with +dragndrop flag, I made some customization to it.
(after! org-download
(setq org-download-heading-lvl nil
org-download-method 'directory
org-download-image-org-width 800))
Verb
Verb is my API testing tool of choice, it’s like postman, but in emacs.
(package! verb)
(use-package! verb
:after org
:config
(map! :localleader :map org-mode-map "v" verb-command-map))
Denote
(package! denote)
(package! denote-journal)
(package! denote-org)
(map! :map doom-leader-notes-map
(:prefix ("d" . "Denote")
:desc "Create new note" "d" #'denote
:desc "Journal: Goto today" "t" #'denote-journal-new-or-existing-entry))
(use-package! denote
:after org
:init
(setq denote-directory (expand-file-name "~/notes")))
(use-package! denote-journal
:after denote
:config
(setq denote-journal-directory (expand-file-name "journal" denote-directory)
denote-journal-title-format 'day-date-month-year)
(defun +denote-agenda-files ()
(append
(seq-filter (lambda (f)
(let ((keywords (denote-extract-keywords-from-path f)))
(or (member "agenda" keywords) (member "tasks" keywords) (member "proj" keywords))))
(denote-directory-files))
(list denote-journal-directory)))
(defun m/update-agenda-files(&rest _)
(setq org-agenda-files (+denote-agenda-files)))
(after! org-agenda
(m/update-agenda-files)
(advice-add 'org-agenda :before #'m/update-agenda-files)
(advice-add 'org-todo-list :before #'m/update-agenda-files)))
(use-package! denote-org :after denote)
Search
(package! consult-notes)
(map! :map doom-leader-notes-map "s" #'consult-notes)
(use-package! consult-notes
:commands (consult-notes)
:config
(consult-notes-denote-mode))
Blogging
My blogging workflow is built on a “static-site-from-org” philosophy. All posts are managed as Denote files, allowing for easy linking and metadata management. The site is generated with ox-publish, driven by my own uncharted package which consolidates the publish project, Denote link export, OpenGraph metadata, and RSS feed into a single, testable place.
Previously this section was a sprawl of loose defvar’s, helper defun’s, and advice-add calls. That logic now lives in uncharted (a local package under ~/projs/elisp/uncharted), leaving this file with just the wiring.
RSS backend
The pipeline relies on org-publish-rss for feed generation. I maintain a custom fork to ensure proper handling of Denote links during export.
(package! org-publish-rss
:recipe (:local-repo "~/projs/elisp/org-publish-rss"))
Uncharted
uncharted wraps the whole authoring-to-publishing flow. uncharted-setup installs the org-publish project (blog posts, static assets, and this literate config as the emacs page), the blog-aware denote: link exporter, and the OpenGraph <meta> builder.
(package! uncharted
:recipe (:local-repo "~/projs/elisp/uncharted"))
(use-package! uncharted
:after denote
:init
(map! :map doom-leader-notes-map :desc "New Blog Post" "b" #'uncharted-new-post)
;; Personal site identity + paths (the package ships only generic defaults).
(setq uncharted-blog-directory (expand-file-name "blog" denote-directory)
uncharted-publishing-directory (expand-file-name "~/projs/mlog/public")
uncharted-site-name "Uncharted"
uncharted-site-url "https://merrick.luois.me"
uncharted-site-description "Uncharted"
uncharted-post-setupfile (expand-file-name "blog/20250420T134203__template.org" denote-directory)
;; Under Doom the literate config lives in `doom-user-dir', not
;; `user-emacs-directory', so publish the `emacs' page from there.
uncharted-emacs-config-directory doom-user-dir)
:config
(uncharted-setup))
I use notmuch for email management. It treats email as a searchable database, which fits my search-first workflow better than traditional folder-based clients. I use mbsync for the actual synchronization, keeping the storage and indexing layers decoupled.
(after! notmuch
(set-popup-rule! "^\\*notmuch" :ignore t)
(map! :map notmuch-show-mode-map
:n "gv" #'goto-address-at-point)
(setq notmuch-saved-searches
'((:name "inbox" :query "tag:inbox not tag:trash" :key "i")
(:name "todo" :query "tag:todo" :key "t")
(:name "flagged" :query "tag:flagged" :key "f")
(:name "sent" :query "tag:sent" :key "s")
(:name "drafts" :query "tag:draft" :key "d")
(:name "bank" :query "tag:bank" :key "b")
(:name "emacs" :query "tag:emacs-devel" :key "e")))
(define-advice notmuch-show-toggle-message (:around (orig-fn &rest args) visit-link-maybe)
"Visit links on point if there are any, otherwise run `notmuch-show-toggle-message'."
(if-let ((url (thing-at-point 'url 'no-properties)))
(goto-address-at-point)
(apply orig-fn args)))
(setq +notmuch-mail-folder "~/.mails"
+notmuch-sync-backend 'mbsync)
(setq message-auto-save-directory "~/.mails/personal/Drafts")
(setq smtpmail-smtp-server "smtp.zoho.com"
smtpmail-smtp-user "merrick@luois.me"
smtpmail-stream-type 'starttls
smtpmail-smtp-service 587
smtpmail-debug-info t
smtpmail-debug-verb t
message-send-mail-function 'message-smtpmail-send-it))
RSS Reader
I use elfeed for information gathering. By managing feeds via elfeed-org, I can treat my reading list as structured data, allowing for better categorization and integration with the rest of my Org-based workflow.
(setq +rss-enable-sliced-images nil)
(after! elfeed
(setq elfeed-search-filter "@2-month-ago +unread -games -news -video -ai -short")
;; prevent nav-flash trigger in elfeed db buffer
(add-to-list '+nav-flash-exclude-modes 'fundamental-mode))
Set the elfeed org file.
(after! elfeed-org
(setq rmh-elfeed-org-files
(list (expand-file-name "20240120T111420--rss-feeds__news_read.org" denote-directory))))
Valign
I use valign to ensure correct alignment of CJK characters in tables. This is particularly important for my reading lists and agenda views where mixed-width characters often break standard monospaced alignment.
(package! valign)
(use-package! valign
:after elfeed
:config
(defun elfeed-search-print-valigned-entry (entry)
"Print valign-ed ENTRY to the buffer."
(let* ((date (elfeed-search-format-date (elfeed-entry-date entry)))
(date-width (car (cdr elfeed-search-date-format)))
(title (or (elfeed-meta entry :title) (elfeed-entry-title entry) ""))
(title-faces (elfeed-search--faces (elfeed-entry-tags entry)))
(feed (elfeed-entry-feed entry))
(feed-title
(when feed
(or (elfeed-meta feed :title) (elfeed-feed-title feed))))
(tags (mapcar #'symbol-name (elfeed-entry-tags entry)))
(tags-str (mapconcat
(lambda (s) (propertize s 'face 'elfeed-search-tag-face))
tags ","))
(title-width (- (window-width) 10 elfeed-search-trailing-width))
(title-column (elfeed-format-column
title (elfeed-clamp
elfeed-search-title-min-width
title-width
elfeed-search-title-max-width)
:left))
(align-to (* (+ date-width 2 (min title-width elfeed-search-title-max-width))
(default-font-width))))
(insert (propertize date 'face 'elfeed-search-date-face) " ")
(insert (propertize title-column 'face title-faces 'kbd-help title) " ")
(valign--put-overlay (1- (point)) (point) 'display (valign--space align-to))
(when feed-title
(insert (propertize feed-title 'face 'elfeed-search-feed-face) " "))
(when tags
(insert "(" tags-str ")"))))
(setq elfeed-search-print-entry-function #'elfeed-search-print-valigned-entry))
Goodies
(package! elfeed-goodies :disable t)
Youtube
(package! elfeed-tube-mpv)
(use-package! elfeed-tube
:after elfeed
:custom ((elfeed-tube-auto-fetch-p t)
(elfeed-tube-backend 'yt-dlp)
(elfeed-tube-auto-save-p t))
:config
(elfeed-tube-setup)
(defun m/elfeed-tube-download (entries)
(interactive
(list
(cond
((eq major-mode 'elfeed-search-mode)
(or (elfeed-search-selected) (ensure-list (elfeed-search-selected))))
((eq major-mode 'elfeed-show-mode)
(ensure-list elfeed-show-entry))
(t (user-error "elfeed-tube-download only works in Elfeed.")))))
(when entries
(if-let* (((seq-every-p #'elfeed-tube--youtube-p entries))
(default-directory "~/videos/yt/"))
(seq-doseq (entry entries)
(let* ((title (elfeed-entry-title entry))
(link (elfeed-entry-link entry))
(output-file (expand-file-name (format "%s.mp4" title) default-directory)))
(unless (file-exists-p output-file)
(let ((proc (start-process
(format "yt-dlp download: %s" title)
(get-buffer-create (format "*elfeed-tube-yt-dlp*: %s" title))
"yt-dlp" "-w" "-c" "-o" "%(title)s.%(ext)s" "-f"
"bestvideo+bestaudio/best/best" "--add-metadata" link)))
(set-process-sentinel
proc
(lambda (process _)
(unless (process-live-p process)
(if (eq (process-exit-status process) 0)
(progn
(message "Finished download: %s" title)
(kill-buffer (process-buffer process))
(elfeed-untag entry 'unread))
(message "Download: [%s] failed" title))))))
(message "Started download: %s" title))))
(message "Not youtube url(s), cancelling download."))))
:bind (:map elfeed-show-mode-map
("F" . elfeed-tube-fetch)
([remap save-buffer] . elfeed-tube-save)
:map elfeed-search-mode-map
("F" . elfeed-tube-fetch)))
(use-package! elfeed-tube-mpv
:after elfeed-tube
:bind (:map elfeed-show-mode-map
("C-c C-f" . elfeed-tube-mpv-follow-mode)
("C-c C-w" . elfeed-tube-mpv-where))
:custom (elfeed-tube-mpv-options '("--ytdl-format=bestvideo[height<=?1080]+bestaudio/best"
"--cache=yes"
"--window-scale=0.5"
"--force-window=yes")))
LLM
integrating llm tools into emacs.
gptel
(after! gptel
(setq gptel-backend (gptel-make-gh-copilot "Copilot")
;; gptel-model 'claude-3.7-sonnet
gptel-model 'claude-sonnet-4))
agent-shell
(package! shell-maker)
(package! acp)
(package! agent-shell)
(use-package agent-shell)
Terminal
I’ve consolidated my terminal workflow around ghostel. It provides the best performance and terminal compatibility within Emacs, and its integration with EVIL makes it feel like a natural extension of the editor rather than a separate environment.
async shell command
(after! emacs
(map! :map doom-leader-map
:desc "Async shell command" "&" 'async-shell-command
:desc "Shell command" "!" 'shell-command))
add bash completion to shell command.
(use-package! bash-completion
:config
(add-hook! 'shell-dynamic-complete-command 'bash-completion-dynamic-complete))
vterm
(use-package! vterm
:defer t
:init
;; Automatically compile the vterm module if needed,
;; though I usually rely on Guix for this.
(setq vterm-always-compile-module t)
:config
(setq vterm-shell (executable-find "fish"))
(add-to-list 'vterm-eval-cmds '("magit" magit-status))
(add-to-list 'vterm-eval-cmds '("man" man))
(add-to-list 'vterm-eval-cmds '("sudo-find-file" doom/sudo-find-file))
(map! :mode vterm-mode :i "C-p" #'vterm-send-C-p)
(map! :mode vterm-mode :i "C-u" #'vterm-send-C-u)
(map! :mode vterm-mode :i "C-n" #'vterm-send-C-n))
ghostel
ghostel is a native terminal emulator powered by libghostty-vt — a Zig dynamic module handles VT parsing, rendering, and PTY I/O. It replaces vterm with better performance and without the maintenance burden of a compiled C module.
(use-package! ghostel
:defer t
:config
(setq ghostel-shell (executable-find "fish"))
(add-to-list 'ghostel-eval-cmds '("magit" magit-status))
(add-to-list 'ghostel-eval-cmds '("man" man))
(add-to-list 'ghostel-eval-cmds '("sudo-find-file" doom/sudo-find-file)))
term-mux
term-mux is my naive attempt to make a tmux like experience in emacs, it’s still in early stage, but it’s working pretty well for me.
(package! term-mux :type 'local :recipe (:local-repo "~/projs/elisp/term-mux"))
(use-package! term-mux
:commands term-mux-toggle
:config
(add-hook 'term-mux-mode-hook (lambda () (mode-line-invisible-mode 1)))
(evil-define-minor-mode-key :n term-mux-mode-map (kbd "`") #'term-mux-command-map)
(evil-define-minor-mode-key :ni term-mux-mode-map (kbd "M-<escape>") #'term-mux-command-map)
;; (setq term-mux-default-terminal-setup-fn #'term-mux--setup-ghostel)
;; (setq term-mux-default-terminal-setup-fn #'term-mux--setup-eshell)
(set-popup-rule! "\\*term-mux" :modeline t :size 0.4 :side 'bottom :ttl nil :quit nil))
global terminal
Dependency for term-mux-frame
(package! term-mux-frame :type 'local :recipe (:local-repo "~/projs/elisp/term-mux"))
Wrapper around term-mux-frame for some special handling in doom.
(use-package! term-mux-frame
:config
(defun terminal-frame ()
"Start a new terminal frame, called from emacsclient."
(unless (getenv "NIRI_SOCKET")
(when-let* ((wayland (getenv "WAYLAND_DISPLAY"))
(runtime (or (getenv "XDG_RUNTIME_DIR")
(format "/run/user/%d" (user-uid))))
(sockets (file-expand-wildcards
(format "%s/niri.%s.*.sock" runtime wayland))))
(setenv "NIRI_SOCKET" (car sockets))))
(doom/switch-to-scratch-buffer)
(term-mux-frame)))
Chinese
Rimel
(package! pyim :disable t)
(package! pyim-liberime :disable t)
(package! rimel :recipe (:host github :repo "emacs-rime/rimel"))
(use-package! rimel
:config
(setq default-input-method "rimel"
rimel-posframe-style 'horizontal)
(setq-default rimel-disable-predicates
'(rimel-predicate-evil-mode-p
;; rimel-predicate-prog-in-code-p
rimel-predicate-org-in-src-block-p))
;; rimel-predicate-after-ascii-char-p
;; rimel-predicate-after-alphabet-char-p
(map! :after org :mode org-mode "C-," #'rimel-convert-string-at-point)
(map! "C-," #'rimel-convert-string-at-point)
)
pyim
(after! pyim
(setq pyim-indicator-modeline-string '("ㄓ" "En"))
(setq pyim-dcache-auto-update nil)
(setq pyim-default-scheme 'rime)
(setq-default pyim-english-input-switch-functions
'(pyim-probe-dynamic-english
pyim-probe-isearch-mode
pyim-probe-program-mode
pyim-probe-evil-normal-mode
pyim-probe-org-structure-template))
(setq-default pyim-punctuation-half-width-functions
'(pyim-probe-punctuation-line-beginning
pyim-probe-punctuation-after-punctuation))
(map! :after org :mode org-mode "C-," #'pyim-convert-string-at-point)
(map! "C-," #'pyim-convert-string-at-point))
Use a slightly narrowing unicode for pangu-spacing.
(package! pangu-spacing :disable t)
(after! pangu-spacing
;; (unsetq-hook! 'org-mode-hook pangu-spacing-real-insert-separtor t)
(setq pangu-spacing-separator "\u2009"))
Translation
(package! fanyi)
(use-package! fanyi
:commands (fanyi-dwim fanyi-dwim2))
Programming
This section contains my specific refinements for development. While Doom’s modules provide excellent defaults, I add custom configurations here to align with my preference for Tree-Sitter based syntax highlighting and specialized LSP integrations (especially for Guile and Elixir).
Common
Common packages work with all languages.
Tree-Sitter
I prefer level 3 font-lock as it provides a good balance between semantic highlighting and visual clarity without being overly distracting.
(after! treesit
(setq treesit-font-lock-level 3
treesit-auto-install-grammar 'ask
treesit-enabled-modes t))
Flymake
Fancy diagnostics for Emacs 31.
(after! flymake
(setq flymake-show-diagnostics-at-end-of-line 'fancy)
(map! :n "[ e" #'flymake-goto-prev-error
:n "] e" #'flymake-goto-next-error))
DevDocs
I use devdocs for offline-capable documentation. It’s integrated into my lookup workflow, allowing for quick reference without leaving the editor.
(package! devdocs)
(use-package! devdocs
:commands (devdocs-lookup devdocs-install)
:init
(setq devdocs-data-dir (expand-file-name "devdocs" doom-cache-dir))
(put 'devdocs-current-docs 'safe-local-variable 'listp)
:config
(set-popup-rule! "^\\*devdocs*\\*" :side 'right :size 0.40 :select t :ttl nil))
Dape
(package! dape)
(use-package! dape
:commands (dape))
(after! dape
(map! :map doom-leader-open-map :desc "Debugger" "d" dape-global-map))
(after! ispell
(setq ispell-dictionary "english")
(ispell-kill-ispell t))
Compilation
(after! compile
(map! :map doom-leader-code-map "k" 'kill-compilation))
LSP/Eglot
I want the help buffers bigger.
(after! eglot
(set-popup-rule! "^\\*eglot-help" :side 'right :size 0.40 :select t :ttl nil))
Setup eldoc-box to help show eglot hover info.
(package! eldoc-box)
(use-package! eldoc-box
:after eglot
:config
(add-hook 'eglot-managed-mode-hook
(lambda ()
(remove-hook 'eldoc-documentation-functions
#'eglot-code-action-suggestion t)))
(add-hook 'eglot-managed-mode-hook #'eldoc-box-hover-at-point-mode t))
Caser
Change between camelCase, snakecase, and kebab-case (dash-case).
(package! caser)
(use-package! caser
:commands (caser-camelcase-dwim caser-snakecase-dwim caser-dashcase-dwim))
C/C++
(after! lsp
(setq lsp-clients-clangd-executable "/home/merrick/.guix-home/profile/bin/clangd"))
(map! :map c-ts-mode-map
:localleader
"t" #'ff-find-other-file
"T" #'ff-find-other-file-other-window)
TypeScript
Use the builtin treesit support for typescript, and tsx.
(after! treesit
(setq treesit-language-source-alist
'((typescript "https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src" nil nil)
(tsx "https://github.com/tree-sitter/tree-sitter-typescript" "master" "tsx/src" nil nil)
(kdl "https://github.com/tree-sitter-grammars/tree-sitter-kdl" "master" "src" nil nil))))
Jump to test file
(defun +typescript/toggle-tests ()
(interactive)
(let ((f (buffer-file-name)))
(find-file
(if (string-match-p "__tests__" f)
(replace-regexp-in-string "__tests__/" "" (replace-regexp-in-string "\\.test" "" f))
(replace-regexp-in-string "/\\([^/]+\\)\\." "/__tests__/\\1.test." f)))))
(map! :after typescript-ts-mode
:map (typescript-ts-mode-map tsx-ts-mode-map)
:localleader
"s" #'+typescript/toggle-tests)
TailwindCSS
TailwindCSS need some lsp.
(package! lsp-tailwindcss :disable t :type 'local :recipe (:local-repo "~/projs/elisp/lsp-tailwindcss"))
(use-package! lsp-tailwindcss
:after lsp-mode
:init
(setq lsp-tailwindcss-add-on-mode t
lsp-tailwindcss-server-version "0.14.27"))
Above is with lsp-mode, and we need something new when working with eglot.
(after! eglot
;; Custom bridge for tailwind/typescript LSP support when using Eglot.
(add-to-list 'eglot-server-programs
'(((js-mode :language-id "javascript") (js-ts-mode :language-id "javascript")
(tsx-ts-mode :language-id "typescriptreact")
(typescript-ts-mode :language-id "typescript")
(typescript-mode :language-id "typescript"))
"rass" "typescript")))
Go
add GOPATH to the lsp library path. #+beginsrc emacs-lisp (after! lsp-go (setq lsp-go-library-directories (vconcat lsp-go-library-directories (vector (expand-file-name “pkg” (getenv “GOPATH”)))))) #+endsrc;
Lua
(after! lua-mode (setq lua-indent-level 2))
OpenGL
(package! glsl-mode)
(use-package! glsl-mode :mode "\\.glsl\\'")
Common Lisp
(package! sly-quicklisp)
(package! sly-asdf)
(use-package! sly-quicklisp :after sly)
(use-package! sly-asdf :after sly)
(after! sly
(defun sly-add-local-project (&optional directory)
"Add the current project to the quick lisp local projects list."
(interactive "D")
(let ((dir (or directory (doom-project-root))))
(thread-last
(format "(pushnew \"%s\" ql:*local-project-directories* :test #'equal)" dir)
(sly-interactive-eval)))))
Clojure
allow jack in babashka repl with shortcut.
(after! cider
(map! :map cider-mode-map :localleader "u" #'cider-jack-in-universal))
Guile Scheme
Geiser keep poping up dialog to ask me what scheme implementation I’m using, I’ll just make it to default to guile.
(after! geiser
(setq-default geiser-scheme-implementation 'guile))
KDL (For Niri)
(package! kdl-ts-mode
:recipe (:host github :repo "merrickluo/kdl-ts-mode"))
;;:recipe (:local-repo "~/projs/elisp/kdl-ts-mode"))
(use-package! kdl-ts-mode
:mode (("\\.kdl\\'" . kdl-ts-mode)))
LSP Booster
(defun lsp-booster--advice-json-parse (old-fn &rest args)
"Try to parse bytecode instead of json."
(or
(when (equal (following-char) ?#)
(let ((bytecode (read (current-buffer))))
(when (byte-code-function-p bytecode)
(funcall bytecode))))
(apply old-fn args)))
(advice-add (if (progn (require 'json)
(fboundp 'json-parse-buffer))
'json-parse-buffer
'json-read)
:around
#'lsp-booster--advice-json-parse)
(defun lsp-booster--advice-final-command (old-fn cmd &optional test?)
"Prepend emacs-lsp-booster command to lsp CMD."
(let ((orig-result (funcall old-fn cmd test?)))
(if (and (not test?) ;; for check lsp-server-present?
(not (file-remote-p default-directory)) ;; see lsp-resolve-final-command, it would add extra shell wrapper
lsp-use-plists
(not (functionp 'json-rpc-connection)) ;; native json-rpc
(executable-find "emacs-lsp-booster"))
(progn
(when-let ((command-from-exec-path (executable-find (car orig-result)))) ;; resolve command from exec-path (in case not found in $PATH)
(setcar orig-result command-from-exec-path))
(message "Using emacs-lsp-booster for %s!" orig-result)
(cons "emacs-lsp-booster" orig-result))
(progn
(message "not using emacs-lsp-booster for: %s!" orig-result)
orig-result))))
(advice-add 'lsp-resolve-final-command :around #'lsp-booster--advice-final-command)
Yuck
(package! yuck-mode)
(use-package! yuck-mode
:mode (("\\.yuck\\'" . yuck-mode)))
Zig
(after! lsp-zig
(setq lsp-zig-zig-lib-path "~/.asdf/installs/zig/0.14.0/lib"))
(after! zig-mode
(advice-add 'zig--run-cmd :around
(lambda (f cmd &optional source &rest args)
(apply f cmd source (append '("--color" "off") args)))))
SQL
Code Format
(after! apheleia
(push '(sqlfmt . ("pg_format" filepath "-s2" "-g")) apheleia-formatters)
(setf (alist-get 'sql-mode apheleia-mode-alist) 'sqlfmt))
Python
(after! (python-mode apheleia)
(setf (alist-get 'python-mode apheleia-mode-alist) 'ruff)
(setf (alist-get 'python-ts-mode apheleia-mode-alist) 'ruff))
Accounting
(after! beancount
(remove-hook! beancount-mode #'flymake-bean-check-enable))
Project Management
I prefer a lightweight project management style that focuses on automation and workspace isolation.
Run make!
Automation is handled via make. I prefer using standard Makefiles for project tasks as they are portable and work well both inside and outside of Emacs.
(map! :map doom-leader-code-map :desc "Run make tasks" "m" #'+make/run)
Tabspaces
I use tabspaces (leveraging the built-in tab-bar-mode) as a replacement for Doom’s default workspace module. I find it much easier to customize and reason about. Each tab represents a project context, allowing for clean separation of buffers and window layouts without the complexity of traditional “workspace” managers.
(after! tab-bar
(setq tab-bar-show nil))
(package! tabspaces :recipe (:host github :repo "mclear-tools/tabspaces"))
(use-package! tabspaces
;; use this next line only if you also use straight, otherwise ignore it.
:hook (after-init . tabspaces-mode) ;; use this only if you want the minor-mode loaded at startup.
:commands (tabspaces-switch-or-create-workspace
tabspaces-open-or-create-project-and-workspace)
:custom
(tabspaces-use-filtered-buffers-as-default t)
(tabspaces-default-tab "main")
(tabspaces-remove-to-default t)
(tabspaces-include-buffers '("*scratch*" "*doom:scratch*" "*Messages*"))
(tabspaces-initialize-project-with-todo nil)
(tabspaces-use-filtered-buffers-as-default nil)
;; (tabspaces-todo-file-name "project-todo.org")
;; sessions
(tabspaces-session t)
(tabspaces-session-auto-restore nil)
(tab-bar-new-tab-choice "*scratch*"))
(map! :map doom-leader-map :desc "Tabspaces" "TAB" #'tabspaces-command-map)
(map! :map tabspaces-command-map :desc "Next Tab" "n" #'tab-next)
(map! :map tabspaces-command-map :desc "Previos Tab" "p" #'tab-previous)
(after! (consult tabspaces)
(defvar consult--source-workspace
(list :name "Workspace Buffers"
:narrow ?w
:history 'buffer-name-history
:category 'buffer
:state #'consult--buffer-state
:default t
:items (lambda () (consult--buffer-query
:predicate #'tabspaces--local-buffer-p
:sort 'visibility
:as #'buffer-name)))
"Set workspace buffer list for consult-buffer.")
(add-to-list 'consult-buffer-sources 'consult--source-workspace))
Special Workspaces
(defun m/tabspaces-move-buffer-to-space (buffer workspace)
"Remove BUFFER from current workspace, and move it to WORKSPACE.
Create the WORKSPACE if not exists."
(let ((tabspaces-remove-to-default nil))
(tabspaces-remove-buffer buffer)
(tabspaces-switch-or-create-workspace workspace)
(switch-to-buffer buffer)))
- Notes
Whenever switching to notes file or open org agenda, goto the notes workspace.
(defvar +notes-workspace-name "*notes*") (defun m/maybe-switch-to-notes-ws () (require 'denote) (when (or (and (buffer-file-name) (denote-file-has-denoted-filename-p (buffer-file-name)) (not (string= (tabspaces--current-tab-name) +notes-workspace-name))) (eq major-mode 'org-agenda-mode)) (m/tabspaces-move-buffer-to-space (current-buffer) +notes-workspace-name))) (add-hook! doom-switch-buffer #'m/maybe-switch-to-notes-ws) (add-hook! find-file #'m/maybe-switch-to-notes-ws) (after! org-agenda (add-hook! org-agenda-mode #'m/maybe-switch-to-notes-ws)) - RSS
(defun =m/rss () "Open elfeed buffer in a tabspaces workspace." (interactive) (tabspaces-switch-or-create-workspace +rss-workspace-name) (unless (memq (buffer-local-value 'major-mode (window-buffer (selected-window))) '(elfeed-show-mode elfeed-search-mode)) (doom/switch-to-scratch-buffer) (elfeed))) (map! :map doom-leader-open-map "s" #'=m/rss) - Email
(defvar +notmuch-workspace-name "*notmuch*" "Name of the workspace created by `=notmuch', dedicated to notmuch.") (defun =m/notmuch () "Open notmuch buffer in a tabspaces workspace." (interactive) (tabspaces-switch-or-create-workspace +notmuch-workspace-name) (if-let* ((win (cl-find-if (lambda (it) (string-match-p "^\\*notmuch" (buffer-name (window-buffer it)))) (doom-visible-windows)))) (select-window win) (funcall +notmuch-home-function))) (defun +notmuch/quit () "Close notmuch buffer and workspace." (interactive) (doom-kill-matching-buffers "^\\*notmuch") (when (string= (tabspaces--current-tab-name) +notmuch-workspace-name) (tabspaces-kill-buffers-close-workspace))) (map! :map doom-leader-open-map :desc "Email" "m" #'=m/notmuch)
Treemacs
(after! treemacs
(setq treemacs-read-string-input 'from-minibuffer))
Entertainment
Emacs is also my portal for lighter activities, from catching up on Hacker News to participating in IRC discussions.
News
(package! hackernews)
(package! lobsters)
(use-package! hackernews :commands (hackernews))
(use-package! lobsters :commands (lobsters-hottest lobsters-newest))
MPV
(package! mpv)
(defun +mpv-play-url-at-point()
"Play the current url in MPV."
(interactive)
(require 'mpv)
(if-let (url (thing-at-point 'url t))
(progn
(message "Lauching mpv with %s" url)
(mpv-play-url url))
(message "no url found at point")))
(defun +elfeed-mpv-play ()
"Play current elfeed in MPV."
(interactive)
(require 'mpv)
(when-let ((entries (elfeed-search-selected)))
(let ((url (elfeed-entry-link (car entries)))) ;; only care about one
(message "Lauching mpv with %s" url)
(mpv-play-url url))))
(map! :map elfeed-search-mode-map "p" #'+elfeed-mpv-play)
Guix
This section documents my integration with the GNU Guix package manager. I prioritize Guix-managed packages for anything with complex native dependencies (like liberime or pdf-tools) to avoid the brittleness of on-the-fly compilation within Emacs and to ensure consistent behavior across my systems.
;; (package! vterm :built-in 'prefer)
(package! liberime :built-in 'prefer)
(package! avy :built-in 'prefer)
(package! nerd-icons :built-in 'prefer)
(package! treemacs-nerd-icons :built-in 'prefer)
(package! notmuch :built-in 'prefer)
(package! pdf-tools :built-in 'prefer)
(package! guix :built-in 'prefer)
(use-package! guix :commands guix)