Wai Hon's Blog

Emacs Config

This is my personal Emacs configuration.



;; Increase the garbage collection threshold to 100MB to reduced startup time.
;; See https://www.reddit.com/r/emacs/comments/3kqt6e
(setq gc-cons-threshold (* 1024 1024 100))

;; Turn off mouse interface early in startup to avoid momentary display
(menu-bar-mode -1)
(tool-bar-mode -1)
(scroll-bar-mode -1)
(tooltip-mode -1)

;; Show column number in the mode line.

Package Management

;; MELPA package
(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)

;; Install 'use-package if not installed.
(unless (package-installed-p 'use-package)
  (package-install 'use-package))

;; For use-package keywords:
;; https://jwiegley.github.io/use-package/keywords/
(eval-when-compile (require 'use-package))

;; Ensure for all packages
(require 'use-package-ensure)
(setq use-package-always-ensure t)

As a reference, these are the common use-package keywords:

Install package
Load after another package
Run code before package loads
Run code after package loads
Set custom variables with concise syntax
Defer loading until some later point
Ensure loading at startup
Bind keys for modes
Set hooks that will cause package to load
Set autoloaded commands that will cause package to load
Activate mode when particular file types are opened

Note: this System Crafter’s note discusses some use-package alternatives.


A popular recommendation is to bind <escape> with keyboard-escape-quit. However, it closes all the other windows at the same time and it is very annoying to me. Instead, I map <escape> to keyboard-quit.

;: Don't forget to handle minibuffer with "minibuffer-keyboard-quit".
(global-set-key (kbd "<escape>") 'keyboard-quit)

An even better way is to build muscle memory to use the standard C-g to “quit” and C-] to “abort”.


I don’t use backup files often as I use git to manage most of my org files. However, I still feel safer when having a backup. I follow the backup configuration from Sacha Chua to enable Emacs’s backups aggressively.

;; Save backup files to a dedicated directory.
(setq backup-directory-alist '(("." . "~/.emacs.d/backups")))
(setq delete-old-versions -1)

;; Make numeric backup versions unconditionally.
(setq version-control t)
(setq vc-make-backup-files t)
(setq auto-save-file-name-transforms '((".*" "~/.emacs.d/auto-save-list/" t)))


(setq inhibit-startup-screen t
      initial-scratch-message ";; scratch\n"
      confirm-kill-emacs 'y-or-n-p)

;; Use year/month/day
(setq calendar-date-style 'iso)

(xterm-mouse-mode +1)


Key Binding

I disable most Emacs’s default keybinding because I am not able to get my muscle memory to work with it. Even if I do, that muscle memory probably will cause trouble when editing in other applications, like web apps.

;; Disabe some Emacs's default keybinding.
(global-unset-key (kbd "C-v"))  ; scroll-up-command
(global-unset-key (kbd "M-v"))  ; scroll-down-command
(global-unset-key (kbd "C-s"))  ; search
(global-unset-key (kbd "C-n"))  ; next line
(global-unset-key (kbd "C-p"))  ; previous line
(global-unset-key (kbd "C-b"))  ; previous char
(global-unset-key (kbd "C-f"))  ; next char
(global-unset-key (kbd "C-t"))  ; switch char
(global-unset-key (kbd "C-l"))  ; recenter
(global-unset-key (kbd "C-j"))  ; (electric-newline-and-maybe-indent)

Text Mode and Prog Mode

I defined my own my/text-mode and my/prog-mode which enable several minor modes and key bindings.

I avoid global settings like global-display-line-numbers-mode and only enable stuff when I need to, like display-line-numbers-mode. I use global settings only if that is globally applicable everywhere.

(defun my/edit-mode ()
  (display-line-numbers-mode +1)
  (visual-line-mode +1)               ; enable "word-wrap"
  (toggle-truncate-lines -1)
  (hl-line-mode +1)
  ; (local-set-key (kbd "C-s") 'save-buffer)  ; learning to use the Emacs's default C-x C-s.
  (local-set-key (kbd "C-S-s") 'write-file)
  (setq cursor-type 'bar)
  (setq show-trailing-whitespace t))

(defun my/text-mode ()
  (goto-address-mode +1)

(defun my/prog-mode ()
  (show-paren-mode +1)
  (goto-address-prog-mode +1)

(add-hook 'text-mode-hook 'my/text-mode)
(add-hook 'prog-mode-hook 'my/prog-mode)
(add-hook 'conf-mode-hook 'my/prog-mode)

Auto Save and Auto Revert

Auto-save and auto-revert makes Emacs more modern. They are especially useful when editing files that is synchronized with other computers. For example, it causes less conflict when editing the same Dropbox file on multiple places.

;; Auto save buffer if idled for 2 seconds.
(setq auto-save-timeout 2)
(auto-save-visited-mode +1)

;; Watch and reload the file changed on the disk.
(global-auto-revert-mode +1)

Modern Editor Behavior

These configurations modernize Emacs.

;; Delete the selected text first before editing.
(delete-selection-mode +1)

;; Mouse middle-click yanks where the point is, not where the mouse is.
(setq mouse-yank-at-point t)

Smooth Scrolling

The default Emacs scrolling behavior is really weird and different from other modern editors.

However, this is still not ideal.

  • Still need more improvement for intuitive scrolling.
;; Smooth Scrolling: https://www.emacswiki.org/emacs/SmoothScrolling
(setq scroll-conservatively 10000
      scroll-step 1)

Moving Lines

Moving lines up and down are very common editing operations to me. This stackoverflow entry has more fancy answers but these two are exactly what I need.

;; move line up
(defun my/move-line-up ()
  (transpose-lines 1)
  (previous-line 2))
(global-set-key [(meta shift up)] 'my/move-line-up)

;; move line down
(defun my/move-line-down ()
  (next-line 1)
  (transpose-lines 1)
  (previous-line 1))

(global-set-key [(meta shift down)] 'my/move-line-down)

Sentence End with Single Space

By default, Emacs treat a period followed by double spaces as the end of sentence. This is old-fashioned and uncommon now.

(setq sentence-end-double-space nil)

Indent with Space

(setq-default indent-tabs-mode nil)


I don’t use Doom Emacs but I do use its theme and modeline.

Doom Theme

(use-package doom-themes
  ;; Global settings (defaults)
  (setq doom-themes-enable-bold t    ; if nil, bold is universally disabled
        doom-themes-enable-italic t) ; if nil, italics is universally disabled

  (setq doom-one-brighter-modeline t
        doom-one-brighter-comments t)

  (setq doom-vibrant-brighter-modeline t
        doom-vibrant-brighter-comments t)

  (load-theme 'doom-one t)

  ;; Enable flashing mode-line on errors

  ;; treemacs
  (setq doom-themes-treemacs-theme "doom-atom")

  ;; Corrects (and improves) org-mode's native fontification.

Note that doom-one-brighter-modeline is particularly useful for the doom-one dark theme. It emphasizes the focused window with a brighter modeline so I don’t get lost in the Emacs frame with multiple windows.

  • However, it seems to be buggy – do not highlight all the time.

Switching Theme

I use doom-one or doom-vibrant (experimental) for dark theme, and doom-one-light for light theme. I switch between light and dark themes based on what I am doing and the ambient lighting.

;; Hydra menu to open common files
(defhydra hydra-switch-theme (:hint nil
                              :foreign-keys warn
                              :exit t)
  "Switch Theme"
  ("l" (load-theme 'doom-one-light t) "Doom One Light")
  ("d" (load-theme 'doom-one t) "Doom One (Dark)")
  ("v" (load-theme 'doom-vibrant t) "Doom Vibrant")
  ("q" nil "Quit Menu"))

(global-set-key (kbd "C-c s") 'hydra-switch-theme/body)

counsel-load-theme is an alternative to the above hydra to switch between all available themes.

Doom Modeline

;; https://github.com/seagle0128/doom-modeline
(use-package doom-modeline
  (doom-modeline-mode +1))

Window Management

Winner Mode

Winner Mode is a global minor mode. When activated, it allows you to “undo” (and “redo”) changes in the window configuration with the key commands C-c left and C-c right.

(use-package winner
  (winner-mode +1))

Window Navigation

;; Navigate between window.
(use-package windmove
  ("M-s-<up>" . 'windmove-up)
  ("M-s-<down>" . 'windmove-down)
  ("M-s-<left>" . 'windmove-left)
  ("M-s-<right>" . 'windmove-right))

(use-package ace-window
  ("M-o" . 'ace-window)
  (aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l))
  (aw-scope 'frame))

Window Dedication

(defun my/toggle-current-window-dedication ()
  (let* ((window (selected-window))
         (dedicated (window-dedicated-p window)))
    (set-window-dedicated-p window (not dedicated))
    (message "Window %sdedicated to %s"
             (if dedicated "no longer " "")
(global-set-key (kbd "<f11>") 'my/toggle-current-window-dedication)

Integration with i3 Window Manager

The idea was originated from SqrtMinusOne. There were also variations like my post and this reddit post.

Display Buffer

I used to assign some buffers to a side window but I no longer does this.

;; Displaying Buffers
;; Documentations
;; - https://www.gnu.org/software/emacs/manual/html_node/elisp/Displaying-Buffers.html
;; - https://www.gnu.org/software/emacs/manual/html_node/elisp/Frame-Layouts-with-Side-Windows.html
;; (defvar parameters
;;   '(window-parameters . ((no-other-window . t)  ;; cannot access side window via C-x o
;;                          (no-delete-other-windows . t)))) ;; cannot delete side windows via C-x 1
;; (setq
;;  display-buffer-alist
;;  `(
;;    ("\\*\\(Tags List\\|Async Shell Command\\)\\*"
;;     display-buffer-reuse-window display-buffer-in-side-window
;;     (side . right)
;;     (slot . 0)
;;     (window-width . fit-window-to-buffer)
;;     (preserve-size . (t . nil))
;;     ,parameters)
;;    ("\\*\\(?:help\\|grep\\|Completions\\)\\*"
;;     display-buffer-reuse-window display-buffer-in-side-window
;;     (side . bottom)
;;     (slot . -1)
;;     (preserve-size . (nil . t))
;;     ,parameters)
;;    ;; exact match
;;    ("\\*\\(?:shell\\|compilation\\|Backtrace\\|Warnings\\)\\*"
;;     display-buffer-reuse-window display-buffer-in-side-window
;;     (side . bottom)
;;     (slot . 1)
;;     (preserve-size . (nil . t))
;;     ,parameters)
;;    ))
;; (global-set-key (kbd "<f10>") 'window-toggle-side-windows)


Counsel / Ivy

The reason I choose ivy over helm is simply because ivy uses the minibuffer and does not leave ton of buffer in Emacs.

;; Ivy is split into three packages: ivy, swiper and counsel;
;; by installing counsel, the other two are brought in as dependencies.
(use-package counsel
  (ivy-mode +1)
  (ivy-count-format "(%d/%d) ")
  (ivy-use-virtual-buffer t)
  (ivy-height 30)
  (ivy-initial-inputs-alist nil "Do not start with ^")
  (ivy-re-builders-alist '((t . ivy--regex-ignore-order)) "ignore order in matching")
  ("M-x" . counsel-M-x)
  ("C-S-p" . counsel-recentf)
  ("C-r" . counsel-bookmark)
  ("C-f" . swiper-isearch)
  (:map swiper-map ([escape] . minibuffer-keyboard-quit))
  (:map ivy-minibuffer-map ([escape] . minibuffer-keyboard-quit)))


which-key displays the key bindings following your currently entered incomplete command (a prefix). I use it on-demand with C-h.

(use-package which-key
  (which-key-idle-delay 10000 "Set idle delay to infinite so it never trigger automatically")
  (which-key-show-early-on-C-h t "Allow C-h to trigger which-key before it is done automatically.")
  (which-key-idle-secondary-delay 0.05)
  (which-key-mode +1 "Non-nil if which-Key mode is enabled"))





;; https://github.com/xuchunyang/youdao-dictionary.el
(use-package youdao-dictionary
  (setq url-automatic-caching t) ; enable cache
  ("C-c v" . youdao-dictionary-search-at-point+)
  ("C-c V" . youdao-dictionary-play-voice-at-point))

(defun browse-dictionary-at-point ()
  (browse-url (concat "https://dictionary.cambridge.org/zht/詞典/英語-漢語-繁體/" (thing-at-point 'word))))
(global-set-key (kbd "M-q") 'browse-dictionary-at-point)


Spell Checking

(use-package flyspell-correct
  :after flyspell
  :bind (:map flyspell-mode-map ("C-;" . flyspell-correct-wrapper)))

(use-package flyspell-correct-ivy
  :after flyspell-correct)


;; https://www.powerthesaurus.org/
(use-package powerthesaurus
  ("M-`" . powerthesaurus-lookup-word-dwim))



There are multiple key-binding to trigger magit-status:

C-x g
The default keybinding from magit.
C-x p m
The keybinding from project.el.
(use-package magit)

Diff Highlight

(use-package diff-hl
  (magit-pre-refresh . diff-hl-magit-pre-refresh)
  (magit-post-refresh . diff-hl-magit-post-refresh))


(use-package vterm
  (define-key vterm-mode-map (kbd "<f1>") nil)
  (define-key vterm-mode-map (kbd "<f2>") nil)
  (define-key vterm-mode-map (kbd "<f3>") nil)
  (define-key vterm-mode-map (kbd "<f4>") nil)
  (define-key vterm-mode-map (kbd "<f5>") nil)
  (define-key vterm-mode-map (kbd "<f6>") nil)
  (define-key vterm-mode-map (kbd "<f7>") nil)
  (define-key vterm-mode-map (kbd "<f8>") nil)
  (define-key vterm-mode-map (kbd "<f9>") nil)
  (define-key vterm-mode-map (kbd "<f10>") nil)
  (define-key vterm-mode-map (kbd "<f11>") nil)
  (define-key vterm-mode-map (kbd "<f12>") nil)
  (vterm-mode . goto-address-mode)
  ("C-c t" . vterm))

Make Script Files Executable Automatically

Make script files (with shebang like #!/bin/bash, #!/bin/sh) executable automatically. See this blog post from Emacs Redux.

(add-hook 'after-save-hook


See this blog post for my Org-Mode workflow for task management.

(use-package org
  ("C-c a" . 'org-agenda)
  ("C-c c" . 'org-capture)
  (:map org-mode-map ("C-c n n" . org-id-get-create))
  (:map org-mode-map ("C-x n n" . org-toggle-narrow-to-subtree))
   '(("n" "Agenda / PROG / NEXT"
      ((agenda "" nil)
       (todo "INTR" nil)
       (todo "PROG" nil)
       (todo "NEXT" nil)
       (todo "TRACK" nil))
     ("r" "Review Pending TODOs"
      ((todo "TODO" nil))
      nil nil)))
  (org-babel-load-languages '((emacs-lisp . t) (python . t) (dot . t) (shell . t)))
  (org-stuck-projects '("+LEVEL=2/-DONE" ("NEXT" "PROG" "TODO") nil ""))
  (org-log-into-drawer t)
  (org-outline-path-complete-in-steps nil)
  (org-priority-default 67)
  (org-priority-lowest 69)
  (org-refile-allow-creating-parent-nodes 'confirm)
  (org-refile-targets '((nil :maxlevel . 3)))
  (org-refile-use-outline-path 'file)
  (org-enforce-todo-dependencies t)
  (org-log-into-drawer t)
  (org-startup-with-inline-images t)
  (org-agenda-skip-deadline-prewarning-if-scheduled 'pre-scheduled nil nil "Hide task until the scheduled date.")
  (org-agenda-span 'day nil nil "My workflow hides all scheduled todo.")
  (org-agenda-todo-ignore-scheduled 'all "Hide all scheduled todo, which are shown on agenda.")
  (org-startup-folded 'content)
  (org-startup-indented t)
  (org-startup-with-inline-images t)
   '((sequence "TODO" "NEXT" "PROG" "INTR" "|" "DONE" "CANCELLED")
     (sequence "TRACK" "|" "DELEGATED")))
  ;; Reserve "C-c <arrow>" for windmove
  (org-mode . (lambda ()
                (local-unset-key (kbd "C-c <left>"))
                (local-unset-key (kbd "C-c <right>"))
                (local-unset-key (kbd "C-c <up>"))
                (local-unset-key (kbd "C-c <down>")))))

Taking Note with Org Roam

;; User Manual: https://www.orgroam.com/manual.html
(use-package org-roam
  (setq org-roam-v2-ack t)
  (org-roam-directory (file-truename "~/org/roam/"))
  (org-roam-completion-everywhere t)
   '(("d" "note" plain "%?"
      (file+head "perm/%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n")
      :unnarrowed t)
     ("r" "reference" plain
      (file "~/org/.templates/reference.org")
      (file+head "ref/%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+filetags: :ref:")
      :unnarrowed t)))
  ("C-c n l" . org-roam-buffer-toggle)
  ("C-c n f" . org-roam-node-find)
  ("C-c n g" . org-roam-graph)
  ("C-c n i" . org-roam-node-insert)
  ("C-c n c" . org-roam-capture)
  ("C-c n r" . org-roam-node-random)
  ("C-c n d" . org-roam-dailies-map)
  (require 'org-roam-dailies)  ; Ensure the keymap is available
  (add-to-list 'display-buffer-alist
               (direction . right)
               (window-width . 0.33)
               (window-height . fit-window-to-buffer))))

(use-package org-roam-ui
  :after org-roam
  :hook (after-init . org-roam-ui-mode)
  (setq org-roam-ui-sync-theme t
        org-roam-ui-follow t
        org-roam-ui-update-on-save t
        org-roam-ui-open-on-start t))

Attach Images with Org Download

(use-package org-download
  (dired-mode-hook org-download-enable)
  (org-download-image-dir "~/org/.download")
  (org-download-timestamp "%F-"))

Blogging with ox-hugo

My blog is generated by Hugo and ox-hugo.

(use-package ox-hugo
  :after ox)

Spaced Repetition with org-drill

I use org-drill to enhance my learning, like vocabulary, reading notes, concepts, etc.

;; https://orgmode.org/worg/org-contrib/org-drill.html
(use-package org-drill
  (org-drill-save-buffers-after-drill-sessions-p nil "Save buffers after drill sessions without prompt.")
  ("C-c d" . org-drill))

I used Anki before embracing Emacs and Org Mode. Anki gave me access on mobile but I have to sync my data to its server. I migrated to org-drill for a more coherent Emacs workflow. To be fair, it gives me access over ssh :-p.


(use-package markdown-mode)

Project Management

Projectile and Project

I use projectile in the first place but also experimenting the built-in project.el.

(use-package project
  :bind-keymap ("C-x p" . project-prefix-map))

;; Note: Customize projectile-project-search-path in local.el.
(use-package projectile
  (projectile-track-known-projects-automatically nil)
  ("C-c p" . projectile-command-map)
  (projectile-mode +1))

I particularly love the handy projectile function projectile-find-file-in-known-projects (C-c p F) which is not supported by project.el.


(with-eval-after-load 'treemacs
  (defun treemacs-ignore (filename absolute-path)
    (or (string-match-p "orig\\'" filename)
        (string-equal ".vscode" filename)))
  (add-to-list 'treemacs-ignored-file-predicates #'treemacs-ignore))

(use-package treemacs
  ("<f9>" . treemacs))

(use-package treemacs-projectile
  :requires (treemacs projectile))

Chinese Support

Rime Input Method

See https://github.com/DogLooksGood/emacs-rime for the setup instruction.

(use-package rime
(setq default-input-method "rime")
(setq rime-user-data-dir "~/.config/ibus/rime")
(rime-show-candidate 'posframe)
(rime-posframe-style 'vertical))