Merrick’s Doom Configuration
About
This is my personal Doom Emacs configuration. Since my entire blog is generated from org files, I thought I’d include my Emacs config as well.
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 (chinese ; wo shuru buliao zhongwen le +rime ; best input method (at least for chinese?) +childframe) ; more like a input method ;;japanese :completion ;;(company ; the ultimate code completion backend ;; +childframe) ; ... when your children are better than you (corfu +icons +orderless) ;;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 ; the search engine of the future :ui ;;deft ; notational velocity for Emacs doom ; what makes DOOM look the way it does doom-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 ;;tabs ; an tab bar for Emacs treemacs ; a project drawer, like neotree but cooler ;;unicode ; extended unicode support for various languages ;;(vc-gutter +diff-hl +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 ;;word-wrap ; soft wrapping with language-aware indent :emacs dired ; making dired pretty [functional] electric ; smarter, keyword-based electric-indent ibuffer ; interactive buffer management 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 :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 +peek) ;;+eglot) llm 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 ;;agda ; types of types of types of types... ;;assembly ; assembly for fun or debugging (beancount +lsp) ; mind the GAAP (cc +lsp) ; 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) ; 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) ; 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 ; ;;json ; At least it ain't XML ;;(java +lsp) ; the poster child for carpal tunnel syndrome (javascript +lsp) ; 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 ; an accounting system in Emacs (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 (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 ;;purescript ; javascript, but functional (python ; beautiful is better than ugly +lsp +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() ;;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 +html +css +lsp) ; the tubes 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 JetBrains Mono.
(setq doom-font "JetBrains Mono-13" doom-variable-pitch-font "Lato") ;;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 vterm-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 vterm.
(after! vterm (defadvice! +vterm--window-body-height-a (orig-fn &optional win pixelwise) "Include line-spacing in the height calculation of the vterm buffer." :around #'window-body-height (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
You can’t really use any other theme after using modus themes, also since dark mode hurt my eyes, so I guess I’m stuck with modus-operandi forever.
(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) (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) (global-subword-mode 1))
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
Org mode is a beast, I’m just starting to use it.
This contains the normal org files, it’s hardly used after migrate everything to denote.
(after! org (setq org-directory "~/syncthing/docs"))
Better defaults
Some random settings for org.
(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 :config (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)))) (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 current blogging setup looks a bit messy, but it’s with a very simple idea: directly generate the whole blog site with org mode files, managed by denote.
Denote
Manage all blog posts to a denote sub directory.
(after! denote (defun m/denote-blog-post () "Create a blog entry in denote sub directory." (declare (interactive-only t)) (interactive) (let ((denote-use-directory (expand-file-name "blog/posts" denote-directory))) (call-interactively 'denote)))) (map! :map doom-leader-notes-map :desc "New Blog Post" "b" #'m/denote-blog-post)
Org Publish
Setup the org publish project, to publish org files to html.
Variables
Setup header/footer for all blog posts.
(defvar m/blog-publishing-directory (expand-file-name "~/projs/mlog/public")) (defvar m/blog-header "<header> <a class=\"title\" href=\"/\"> <h2>Uncharted Mind Space</h2> </a> <nav> <a href=\"/\">Home</a> <a href=\"/about\">About</a> <a href=\"/emacs\">Emacs</a> </nav> </header>") (defvar m/blog-footer "<footer> <p>Powered by <a href=\"https://orgmode.org/\">Org Mode</a></p> </footer>") (defvar m/blog-meta "<link rel=\"stylesheet\" type=\"text/css\" href=\"static/style.css\"/>")
Support Functions
Helper functions to generate correct links.
(defun org-export-file-name (path) "Get the export file name for org file." (with-temp-buffer (insert-file-contents path) (org-mode) (org-export-output-file-name ""))) (defun org-export-file-path (path) "Return the export file path for PATH." (let ((dir (file-name-directory path)) (name (org-export-file-name path))) (expand-file-name name dir))) (defun m/denote-link-ol-export (link description format) "Export a `denote:' link from Org files. The LINK, DESCRIPTION, and FORMAT are handled by the export backend." (pcase-let* ((`(,path ,query ,file-search) (denote-link--ol-resolve-link-to-target link :full-data)) (anchor (when path (file-relative-name (org-export-file-path path)))) ;;(anchor (when path (file-relative-name (file-name-sans-extension path)))) (desc (cond (description) (file-search (format "denote:%s::%s" query file-search)) (t (concat "denote:" query))))) (if path (pcase format ('html (if file-search (format "<a href=\"%s.html%s\">%s</a>" anchor file-search desc) (format "<a href=\"%s.html\">%s</a>" anchor desc))) ('latex (format "\\href{%s}{%s}" (replace-regexp-in-string "[\\{}$%&_#~^]" "\\\\\\&" path) desc)) ('texinfo (format "@uref{%s,%s}" path desc)) ('ascii (format "[%s] <denote:%s>" desc path)) ('md (format "[%s](%s)" desc path)) (_ path)) (format-message "[[Denote query for `%s']]" query)))) (advice-add 'denote-link-ol-export :override #'m/denote-link-ol-export)
Opengraph
Add meta headers for opengraph, make the link looks nicer in social media sites.
(after! org (defun m/blog-opengraph-meta(info) "Build Open Graph meta fields for html." (append (org-html-meta-tags-default info) (let* ((site-name "Uncharted Mind Space") (parsed-tree (plist-get info :parse-tree)) (first-heading (org-element-map parsed-tree 'headline #'identity nil t)) (section-content (org-element-property :contents-begin first-heading)) (raw-content (plist-get info :input-buffer)) (description (when section-content (let ((text (with-current-buffer raw-content (buffer-substring-no-properties section-content (min (+ section-content 100) (buffer-size)))))) (if (> (length text) 97) (concat (substring text 0 97) "...") text))))) `(("name" "description" ,(or description "")) ("property" "og:locale" ,(m/org-element-plain-text info :language)) ("property" "og:title" ,(m/org-element-plain-text info :title)) ("property" "og:type" "article") ("property" "og:description" ,(or description "")) ("property" "og:site_name" ,site-name))))) (setq org-html-meta-tags #'m/blog-opengraph-meta))
Project
Finally, link all setup together with the org publish project setup.
(after! ox-publish (setq org-publish-project-alist `(("blog" :components ("blog-posts" "blog-static" "blog-emacs")) ("blog-emacs" :base-directory ,doom-user-dir :base-extension "org" :publishing-function org-html-publish-to-html :publishing-directory ,(expand-file-name "" m/blog-publishing-directory) :html-head ,m/blog-meta :html-preamble ,m/blog-header :html-postamble ,m/blog-footer :html-link-org-files-as-html t) ("blog-posts" :base-directory ,(expand-file-name "blog" denote-directory) :base-extension "org" :recursive t :publishing-function org-html-publish-to-html :exclude ,(regexp-opt '("draft" "template" "sitemap")) :publishing-directory ,(expand-file-name "" m/blog-publishing-directory) :html-head ,m/blog-meta :html-preamble ,m/blog-header :html-postamble ,m/blog-footer :html-link-org-files-as-html t :auto-rss t :rss-title "Uncharted Mind Space" ;; TODO: use a variable :rss-description "Hello" :rss-link "https://merrick.luois.me" :rss-filter-function m/blog-filter-rss :rss-with-content all :completion-function org-publish-rss) ("blog-static" :base-directory ,(expand-file-name "blog/static" denote-directory) :base-extension ,(regexp-opt '("css" "png" "svg")) :recursive t :publishing-directory ,(expand-file-name "static" m/blog-publishing-directory) :publishing-function org-publish-attachment))))
RSS
Uses org-publish-rss for easier rss generation instead of hacking with ox-rss and sitemap, my custom fork to help export the denote links.
(package! org-publish-rss :recipe (:local-repo "~/projs/elisp/org-publish-rss"))
(use-package! org-publish-rss :after org :config (defun m/gen-rss-link-name (file) (concat "posts/" (org-export-file-name file))) (setq org-publish-rss-publish-immediately t) (setq org-publish-rss-link-method #'m/gen-rss-link-name)) (defun m/blog-filter-rss (path) (not (string-match-p (regexp-opt '("index" "template" "page")) path))) (after! (ox-publish org-publish-rss) (defun m/org-element-plain-text (info key) (org-html-plain-text (org-element-interpret-data (plist-get info key)) info)))
I use notmuch to manage my emails, and I use mbsync to sync my emails from the server.
(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"))) (defadvice! +notmuch-visit-link-maybe (orig-fn &rest args) "Visit links on point if there are any, otherwise run `notmuch-show-toggle-message'." :around #'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 to read RSS feeds, and elfeed-org to manage my feeds.
(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
valign can help align cjk characters in table like buffers in org mode or elfeed.
(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[height<=?1080]+bestaudio/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))
Terminal
After playing around with all the terminal emulators and eshell, I decided to stick with vterm, it’s fast, and it works really well with EVIL.
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 (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))
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 () (hide-mode-line-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-vterm) ;; (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." (doom/switch-to-scratch-buffer) (term-mux-frame)))
Chinese
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 is emacs’s major usage for me after all. Doom modules already have most programming languages setup, I just need to add some minor tweaks.
Common
Common packages work with all languages.
Tree-Sitter
Lv4 is too much for me.
(after! treesit (setq treesit-font-lock-level 3))
Breadcrumb
(package! breadcrumb :disable t)
(use-package! breadcrumb :config (breadcrumb-mode))
DevDocs
This is the best devdocs integration I can find.
(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))
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)))) (use-package! typescript-ts-mode :mode (("\\.ts\\'" . typescript-ts-mode) ("\\.tsx\\'" . tsx-ts-mode)) :config (add-hook! '(typescript-ts-mode-hook tsx-ts-mode-hook) #'lsp!))
TailwindCSS
TailwindCSS need some lsp.
(package! lsp-tailwindcss :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"))
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"))
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
Run make!
(map! :map doom-leader-code-map :desc "Run make tasks" "m" #'+make/run)
Tabspaces
Replaces doom builtin workspaces, which is really hard to tweak and use, tabspaces is also the target package used in the next workspace module rewrite, so it’s very promising. I’m also having a
(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 t) (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 () (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
(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
News
(package! hackernews) (package! lobsters)
(use-package! hackernews :commands (hackernews)) (use-package! lobsters :commands (lobsters-hottest lobsters-newest))
Social Network
(after! circe (set-irc-server! "irc.libera.chat" `(:tls t :port 6697 :nick "merrick" :sasl-username "merrick" :sasl-password (lambda (_server) (password-store-get "merrick/irc.libera.chat/merrick")) :channels ("#emacs" "#guix" "#guixcn"))))
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
Guix provides excellent management for Emacs packages, particularly those with native dependencies. Using Guix-managed packages instead of installing through straight.el is often more reliable. Mark these packages here for easier reference and maintenance.
(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)