Uncharted Mind Space

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)
  • Templates
    (after! denote
      (setq denote-templates
            '((journal . "#+category: Journal\n\n"))))
    
  • Agenda integration
    (after! denote-journal
      (add-to-list 'org-agenda-files (denote-journal-directory)))
    

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)))

Email

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)