Wai Hon's Blog

Emacs Config

This is my personal Emacs configuration.

General

Startup

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

Package Management

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

;; Install 'use-package if not installed.
(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (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:

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

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

Quitting

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”.

Backups

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

Miscellaneous

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

Editing

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)
  (local-set-key (kbd "C-S-s") 'write-file)
  (setq cursor-type 'bar))

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

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

(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 ()
  (interactive)
  (transpose-lines 1)
  (previous-line 2))
(global-set-key [(meta shift up)] 'my/move-line-up)

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

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

Appearance

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

Doom Theme

(use-package doom-themes
  :config
  ;; 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-light t)

  ;; Enable flashing mode-line on errors
  (doom-themes-visual-bell-config)

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

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

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
  :init
  (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
  :init
  (winner-mode +1))

Window Navigation

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

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

Display Buffer

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

Window Dedication

(defun my/toggle-current-window-dedication ()
  (interactive)
  (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 " "")
	     (buffer-name))))
(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.

Discoverability

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
  :init
  (ivy-mode +1)
  :custom
  (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")
  :bind
  ("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

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
  :custom
  (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"))

Hydra

https://github.com/abo-abo/hydra/wiki/Hydras-by-Topic

Reading

Dictionary

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

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

Writing

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)

Synonyms

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

Programming

Magit

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
  :init
  (global-diff-hl-mode)
  :hook
  (magit-pre-refresh . diff-hl-magit-pre-refresh)
  (magit-post-refresh . diff-hl-magit-post-refresh))

Terminal

(use-package vterm
  :config
  (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)
  :hook
  (vterm-mode . goto-address-mode)
  :bind
  ("C-`" . 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
	  'executable-make-buffer-file-executable-if-script-p)

Org-Mode

(use-package org
  :bind
  ("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))
  :custom
  (org-agenda-custom-commands
   '(("n" "Agenda / PROG / NEXT"
      ((agenda "" nil)
       (todo "INTR" nil)
       (todo "PROG" nil)
       (todo "NEXT" nil))
      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)
  (org-todo-keywords
   '((sequence "TODO(t)" "NEXT(n)" "PROG(p)" "INTR(i)" "|" "DONE(d)")
     (sequence "|" "CANCELLED(c)")))
  :hook
  ;; 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>")))))

(use-package org-download
  :hook
  (dired-mode-hook org-download-enable)
  :custom
  (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)

Taking Note with Org Roam

;; User Manual: https://www.orgroam.com/manual.html
(use-package org-roam
  :init
  (setq org-roam-v2-ack t)
  :custom
  (org-roam-directory (file-truename "~/org/roam/"))
  (org-roam-completion-everywhere t)
  (org-roam-capture-templates
   '(("d" "note" plain "%?"
      :if-new
      (file+head "perm/%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n")
      :unnarrowed t)
     ("r" "reference" plain
      (file "~/org/.templates/reference.org")
      :if-new
      (file+head "ref/%<%Y%m%d%H%M%S>-${slug}.org" "#+title: ${title}\n#+filetags: :ref:")
      :unnarrowed t)))
  :bind
  ("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)
  ;; :bind-keymap
  ;; ("C-c n d" . org-roam-dailies-map)
  :config
  (org-roam-db-autosync-mode)
  ;; (require 'org-roam-dailies)  ; Ensure the keymap is available
  (add-to-list 'display-buffer-alist
	     '("\\*org-roam\\*"
	       (display-buffer-in-direction)
	       (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)
  :config
  (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))

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
  :custom
  (org-drill-save-buffers-after-drill-sessions-p nil "Save buffers after drill sessions without prompt."))

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.

Markdown

(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
  :custom
  (projectile-track-known-projects-automatically nil)
  :bind-keymap
  ("C-c p" . projectile-command-map)
  :config
  (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.

Treemacs

(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
  :bind
  ("<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
:init
(setq default-input-method "rime")
(setq rime-user-data-dir "~/.config/ibus/rime")
:custom
(rime-show-candidate 'posframe)
(rime-posframe-style 'vertical))