Wai Hon's Blog

Emacs Config

This is my publicly available Emacs configuration, updated regularly. I hope that it will be helpful for both new and experienced Emacs users.

Startup

Enable Lexical Binding

Lexical binding is a feature of Emacs Lisp that allows variables to be bound to values in a lexical scope, rather than a dynamic scope. It help to improve the readability, maintainability, performance, and security of the code.

To enable lexical binding to an elisp file, set lexical-binding to the prop line. For example, this is the first line of my init.el.

;; -*- lexical-binding: t; -*-

To enable lexical binding in org-babel, set the :lexical to t in the header

(setq org-babel-default-header-args:emacs-lisp '((:lexical . "yes")))

To verify whether lexical binding is enabled or not for org babel, evaluate this org src block. If enabled, the return value is closure (t) nil. Otherwise, it is (lambda nil).

#+begin_src elisp
(lambda ())
#+end_src

Reduce Startup Time with Less Garbage Collection

To reduce the startup time of Emacs, you can increase the garbage collection threshold to 100MB. See https://www.reddit.com/r/emacs/comments/3kqt6e. This reduced my startup time from 4.3s to 3.1s.

To check the startup time, use (emacs-init-time). To check the number of garbage collections performed, see the value of gcs-done.

(setq gc-cons-threshold (* 1024 1024 100))

Save Customization Out of the Init File

My init.el is generated by org babel. If I save my customization in the init.el, they will be lost after org-babel-tangle.

(setq custom-file (locate-user-emacs-file "custom.el"))
;; create custom.el if it does not exists.
(unless (file-exists-p custom-file)
  (write-region "" nil custom-file))
(load custom-file)

Package Management

Add “melpa” to Package Archives

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

Manage Package with use-package

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

;; See https://github.com/slotThe/vc-use-package
;; TODO: remove when vc-use-package is merged.
(unless (package-installed-p 'vc-use-package)
  (package-vc-install "https://github.com/slotThe/vc-use-package"))

Disable Native Compilation Warnings

Disable the annoying warning messages while Emacs is compiling packages in the background.

(setq native-comp-async-report-warnings-errors nil)

Editing

Backup Files

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 `(("." . ,(expand-file-name "backups" user-emacs-directory))))

;; Use copying to create backup files.
;;
;; The default is nil, which means Emacs moves file to backup and then copy it
;; back. First seen on https://idiomdrottning.org/bad-emacs-defaults.
(setq backup-by-copying t)

;; Make numeric backup versions unconditionally.
(setq version-control t)

;; No not delete backup files.
(setq delete-old-versions -1)

;; Back up files even covered by version control.
(setq vc-make-backup-files t)

Quitting and Aborting

Here are some standard ways of quitting:

C-g
Quit cancel running or partially typed command (keyboard-quit)
C-]
Abort innermost recursive editing level and cancel the command which invoked it (abort-recursive-edit).
ESC ESC ESC
Either quit or abort, whichever makes sense (keyboard-escape-quit).

I have trained my muscle memory to use C-g to “quit” and C-] to “abort”. I don’t use and don’t like ESC ESC ESC because it has the side effect of closing all other windows. I map ESC to keyboard-quit so it never reach 3 consecutive ESC.

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

;; Avoid entering the ~repeat-complex-cammand~ when quiting everything with ~C-x~.
(global-unset-key (kbd "C-x <escape> <escape>")) ; repeat-complex-command

Unset Unused Key Bindings

To avoid accidentally activating keybindings that I rarely use, I unset some of Emacs’s default key bindings.

;; Disabe some Emacs's default keybindings.
(dolist (key '("C-v"    ; scroll-up-command    (Chrome OS: Launcher + <up>)
               "M-v"    ; scroll-down-command  (Chrome OS: Launcher + <down>)
               "C-n"    ; next-line            (Chrome OS: <down>)
               "C-p"    ; previous-line        (Chrome OS: <up>)
               "C-f"    ; next-char            (Chrome OS: <right>)
               "C-b"    ; previous-char        (Chrome OS: <left>)
               "C-s"    ; isearch-forward
               "C-t"    ; switch-char
               "C-l"    ; recenter
               "C-j"    ; electric-newline-and-maybe-indent
               "M-l"    ; downcase-word
               "M-u"    ; upcase-wrod
               "C-z"))  ; suspend-frame
  (global-unset-key (kbd key)))

;; Disable the "numeric argument". When used, I use the =C-u= prefix.
(dolist (prefix '("M-" "C-M-"))
  (global-unset-key (kbd (concat prefix "-")))
  (dotimes (i 10)
    (global-unset-key (kbd (concat prefix (number-to-string i))))))

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 in prog-mode. I use global settings only if that is globally applicable everywhere.

(defun my/edit-mode ()
  ;; (hl-line-mode +1)
  (local-set-key (kbd "C-S-s") 'write-file)
  (setq cursor-type 'bar)
  (setq show-trailing-whitespace t))

(defun my/text-mode ()
  (my/edit-mode)
  (visual-line-mode +1)               ; enable "word-wrap"
  (toggle-truncate-lines -1)
  (goto-address-mode +1)
  (flyspell-mode))

(defun my/prog-mode ()
  (my/edit-mode)
  (setq truncate-lines nil)
  (display-line-numbers-mode +1)
  (show-paren-mode +1)
  (goto-address-prog-mode +1)
  (flyspell-prog-mode)
  (hl-line-mode +1)
  (display-fill-column-indicator-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)

;; Removes the overlay properties which flyspell uses on incorrect words for mouse operations.
;; https://emacs.stackexchange.com/a/55708
(defun make-flyspell-overlay-return-mouse-stuff (overlay)
  (overlay-put overlay 'help-echo nil)
  (overlay-put overlay 'keymap nil)
  (overlay-put overlay 'mouse-face nil))
(advice-add 'make-flyspell-overlay :filter-return #'make-flyspell-overlay-return-mouse-stuff)

Delete Trailing Whitespaces Except Current Line

I hook this function with save-buffer.

The before-save-hook does not work in a specific scenario:

  1. When the current line has trailing space.
  2. After saving the file (the trailing space is not removed, as expected).
  3. Move the cursor to the next line.
  4. Save the file again, the before-save-hook is not triggered because the file has not been altered from the last save.
;; Remove trailing whitespace except current line.
;; https://stackoverflow.com/a/35781486/1747877
(defun my/delete-trailing-whitespace-except-current-line ()
  "Delete trailing whitespace in the whole buffer, except on the current line.
  The current line exception is because we do want to remove any whitespace
  on the current line on saving the file while we are in-between typing something.

  Do not do anything if `do-not-delete-trailing-whitespace' is non-nil."
  (interactive)
  (when (not (bound-and-true-p do-not-delete-trailing-whitespace))
    (delete-trailing-whitespace (point-min) (line-beginning-position))
    (delete-trailing-whitespace (line-end-position) (point-max))))

Better save-buffer

I made 2 improvements to save-buffer:

  1. do not save for non-file-visiting buffer (See my blog post)
  2. save asynchronously with thread to avoid blocking the UI
(defvar my/save-buffer-with-thread t
  "If non-nil, execute save-buffer with thread so it does not block the UI")

(defun my/save-buffer-advice (orig-fun &rest args)
  (unless (or (buffer-file-name)                       ; regular buffer
              (buffer-file-name (buffer-base-buffer))) ; indirect buffer
    (user-error "Use 'M-x write-file' to save this buffer."))

  (my/delete-trailing-whitespace-except-current-line)

  (if (and my/save-buffer-with-thread
           (not (file-remote-p default-directory))
           ;; Editing with a thread can prevent a buffer from being killed.
           ;; Disable threading on `with-editor-mode' because its
           ;; `with-editor-finish' need to kill the buffer.
           (not (bound-and-true-p with-editor-mode)))
      (make-thread
       (condition-case err
           (apply orig-fun args)
         (error
          (message "Error from my/save-buffer-advice %S" err)
          nil))
       "my/save-buffer-advice")
    (apply orig-fun args)))

(advice-add 'save-buffer :around #'my/save-buffer-advice)

Auto Save and Auto Revert

Auto-save and auto-revert cause less conflict when editing files synchronized on multiple computers.

;; auto-save file name conversion.
(setq auto-save-file-name-transforms `((".*" ,(expand-file-name "auto-save-list" user-emacs-directory) t)))

;; 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.
(setq auto-revert-remote-files t)
(global-auto-revert-mode 1)

;; Do not generate any messages.
(setq auto-revert-verbose nil)

;; Do not create lock files (prefix ".#").
(setq create-lockfiles nil)

;; Use a timer instead of file notification.
;;
;; (setq auto-revert-use-notify nil
;;       auto-revert-interval 2)

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, the result is still not ideal. We might need to wait for the pixel-based scrolling coming with Emacs 29.

;; Scroll just enough text to bring point into view and never centers point.
(setq scroll-conservatively 101)

(if (fboundp 'pixel-scroll-precision-mode)
    (pixel-scroll-precision-mode 1)
  ;; 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)

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)

Rename File and Buffer Together

See https://emacs.readthedocs.io/en/latest/file_management.html.

;; rename-visited-file is introduced in Emacs 29.
(unless (fboundp 'rename-visited-file)
  (defun rename-visited-file ()
    "Renames the current buffer and the file it is visiting."
    (interactive)
    (let ((name (buffer-name))
          (filename (buffer-file-name)))
      (if (not (and filename (file-exists-p filename)))
          (error "Buffer '%s' is not visiting a file!" name)
        (let ((new-name (read-file-name "New name: " filename)))
          (if (get-buffer new-name)
              (error "A buffer named '%s' already exists!" new-name)
            (rename-file filename new-name 1)
            (rename-buffer new-name)
            (set-visited-file-name new-name)
            (set-buffer-modified-p nil)
            (message "File '%s' successfully renamed to '%s'"
                     name (file-name-nondirectory new-name))))))))

(global-set-key (kbd "C-x R") #'rename-visited-file)

Delete File and Buffer Together

See http://emacsredux.com/blog/2013/04/03/delete-file-and-buffer/. If you like this command, it worth taking a look at crux package as well for similar useful collection.

(defun my/delete-file-and-buffer ()
  "Kills the current buffer and deletes the file it is visiting."
  (interactive)
  (if-let ((filename (buffer-file-name)))
      (when (y-or-n-p (concat "Do you really want to delete file " filename " ?"))
        (delete-file filename)
        (message "Deleted file %s." filename)
        (kill-buffer))
    (message "Not a file visiting buffer!")))

(global-set-key (kbd "C-x K") #'my/delete-file-and-buffer)

Edit File as Root

(defun my/sudo-find-file (file-name)
  "Like find file, but opens the file as root."
  (interactive "FSudo Find File: ")
  (let ((tramp-file-name (concat "/sudo::" (expand-file-name file-name))))
    (find-file tramp-file-name)))

Case-sensitive replace-string

M-x replace-string preserves case in each match if case-replace (preserve case) and case-fold-search (ignore case) are non-nil. The latter makes replace-string case-insensitive.

I prefer case-sensitive replace-string and apply the customization from https://stackoverflow.com/a/5346216.

(defun with-case-fold-search (orig-fun &rest args)
  (let ((case-fold-search nil))
    (apply orig-fun args)))

(advice-add 'replace-string :around #'with-case-fold-search)

wgrep

(use-package wgrep)

Miscellaneous

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

(xterm-mouse-mode +1)

;; Remember and restore the last cursor location of opened files
(save-place-mode 1)

;; Remeber recent files.
(recentf-mode +1)

;; Don't pop up UI dialogs when prompting
(setq use-dialog-box nil)

;; Disable the alarm bell (https://www.emacswiki.org/emacs/AlarmBell).
(setq ring-bell-function 'ignore)

;; Use shorter "y" or "n" to confirm killing emacs.
(setq confirm-kill-emacs 'y-or-n-p)

Appearance

Font and Font Size Scaling

My default font is “Hack” or “Meslo”. I also use “WenQuanYi Micro Hei” for Chinese, Japanese, and Korean characters (CJK) and “Noto Color Emoji” for emojis.

Different fonts can have different widths and heights, even when they are the same size. For example, size 10 of font A might be bigger than size 10 of font B. This can make things look uneven. I rescale the font with face-font-rescale-alist so that they have the same width (I might get the same height if I am lucky). This makes things look better, especially in tables in org mode.

With Same Font SizeWith Rescaled Font Sizes

Here are the fonts and scale factors I used on the right side above:

TypeFontScale Factors
DefaultHack1
CJKWenQuanYi Micro Hei1.2
EmojiNoto Color Emoji0.95
(defun my/set-fonts (default-font-name
                     default-font-height
                     cjk-font-name
                     cjk-font-scale
                     emoji-font-name
                     emoji-font-scale)
  "Helper function to set the default, CJK and Emoji fonts."
  ;; Set the default font
  (when (member default-font-name (font-family-list))
    (set-face-attribute 'default nil
                        :family default-font-name
                        :height default-font-height)
    (set-frame-font default-font-name nil t))

  ;; Set the CJK font in the default fontset.
  (when (member cjk-font-name (font-family-list))
    (dolist (script (list 'han 'kana 'cjk-misc))
      (set-fontset-font t script cjk-font-name)))

  ;; Set the Emoji font in the default fontset.
  (when (member emoji-font-name (font-family-list))
    (set-fontset-font t 'emoji emoji-font-name))

  ;; Rescale the CJK and emoji fonts.
  (setq face-font-rescale-alist
        `((,(format ".*%s.*" cjk-font-name) . ,cjk-font-scale)
          (,(format ".*%s.*" emoji-font-name) . ,emoji-font-scale))))

;; Different computers might need different scaling factors with the
;; same fonts.
(cond
 ;; MacOS
 ((eq system-type 'darwin)
  (my/set-fonts
   "Menlo" 140
   "Hiragino Sans CNS" 1.2
   "Apple Color Emoji" 0.9))
 ;; Arch Linux
 ((string= (system-name) "arch-penguin")
  (my/set-fonts
   "Hack" 95
   "WenQuanYi Micro Hei" 1.25
   "Noto Color Emoji" 1.00))
 ;; Default (Assume Debian-Based Linux)
 (t
  (my/set-fonts
   "Hack" 95
   "WenQuanYi Micro Hei" 1.2
   "Noto Color Emoji" 0.95)))

Customize the Face for Modes

My original use case is to enlarge the font size in modes for extensive reading.

(defun my/rescale-height (ratio)
  (let* ((base-height (face-attribute 'default :height)))
    (round (* ratio base-height))))

(defun my/adjust-face-for-modes ()
  "Adjust the face based on the major mode."
  (pcase major-mode
    ('elfeed-show-mode (buffer-face-set :height (my/rescale-height 1.25)))
    ('Info-mode (buffer-face-set :height (my/rescale-height 1.25)))))

(add-hook 'after-change-major-mode-hook #'my/adjust-face-for-modes)

Modus Theme

Note that there is a command M-x modus-themes-toggle to toggle the dark and light modus theme. Thanks to this video. I found the Modus themes is less buggy than doom theme, with the which-key package.

(use-package modus-themes
  :init
  (setq modus-themes-to-toggle '(modus-operandi modus-vivendi-tinted))
  (setq modus-themes-headings
        '((agenda-date . (1.2))))

  (setq modus-themes-common-palette-overrides
        '(;; Heading
          (fg-heading-1 blue)
          (fg-heading-2 cyan)
          (fg-heading-3 magenta)
          (fg-heading-4 blue)

          ;; Org Block
          (bg-prose-block-contents bg-blue-nuanced)
          (bg-prose-block-delimiter bg-lavender)
          (fg-prose-block-delimiter fg-main)

          ;; Tab Bar / Tab Line
          (bg-tab-bar bg-main)
          (bg-tab-current bg-active)
          (bg-tab-other bg-dim)))

  (defun my/modus-themes-custom-faces (&rest _)
    (modus-themes-with-colors
      (custom-set-faces
       ;; Add "padding" to the header line
       `(header-line ((,c
                       :underline (:style line :position t :color ,border)
                       :box (:line-width 2 :color ,bg-dim))))

       ;; Active Tab Line
       `(tab-line ((,c
                    :underline (:style line :position t :color ,border)
                    :box (:line-width 10 :color ,bg-dim))))

       ;; Inactive Tab Line
       `(tab-line-tab ((,c
                        :underline (:style line :position t :color ,border)
                        :box (:line-width 10 :color ,bg-dim))))

       `(tab-line-tab-inactive ((,c
                                 :underline (:style line :position t :color ,border)
                                 :box (:line-width 10 :color ,bg-dim))))

       )))
  (load-theme 'modus-vivendi-tinted :no-confirm)
  (my/modus-themes-custom-faces)
  (add-hook 'modus-themes-after-load-theme-hook #'my/modus-themes-custom-faces)
  :bind
  ("<f5>" . modus-themes-toggle)
  ("<f6>" . modus-themes-toggle))

IDE Layout with Side Windows

(setq my/side-window-alist
      `((0 . ,(rx (or "*compilation*"
                      "*eshell*"
                      "*Help*"
                      "*llm*"
                      "*shell*"
                      "*vterm*"
                      "*xref*"
                      "*Messages*"
                      "magit: "
                      "COMMIT_EDITMSG"
                      "fig: "
                      "*Google Translate*")))
        (1 . ,(rx (or "*vterm*")))))

(setq display-buffer-alist nil)

(dolist (config my/side-window-alist)
  (let* ((config-slot (car config))
         (config-buffer-names (cdr config)))
    (add-to-list 'display-buffer-alist
                 `(,config-buffer-names
                   (display-buffer-in-side-window)
                   (side . right)
                   (slot . ,config-slot)
                   (dedicated . t)
                   (window-width . 0.333)
                   (window-parameters . ((no-other-window . nil) ; disable because it makes me easier to switch window
                                         (no-delete-other-windows . t)))))))

(global-set-key (kbd "M-`") #'window-toggle-side-windows)

(defun my/has-side-windows ()
  (not (frame-root-window-p (window-main-window))))

(defun my/get-window-side ()
  "Return the side of the current window."
  (window-parameter (frame-selected-window) 'window-side))

;; Sets default-directory to org-directory so that =M-x magit= from the agenda
;; view does not ask me for a dir.
(defun my/org-agenda-advice (orig-fun &rest args)
  (let ((default-directory org-directory))
    ;; If on a side window, switch away.
    ;; Otherwise, `org-agenda' throws error "cannot make side window the only window"
    (when (my/get-window-side) (other-window 1))
    (apply orig-fun args)))
(advice-add 'org-agenda :around #'my/org-agenda-advice)

;; [experimental] if possible, only enable in side windows.
;; (global-tab-line-mode)

Display Tooltip in the Echo Area

Whether to see help messages next to the cursor or in the echo area. I prefer echo area in general but it does not play well with long text.

tooltip-mode on (1)tooltip-mode off (-1)
(tooltip-mode 1)

Breadcrumbs are sequences of short strings indicating where you are in some big tree-like maze. I use joaotavora/breadcrumb which shows the file relative path to the project and the imenu from the top level heading down to the current one.

(use-package breadcrumb
  :config
  (setq breadcrumb-imenu-max-length 1.0)
  (setq breadcrumb-project-max-length 1.0)

  ;; Make Org heading style the same.
  ;; https://github.com/joaotavora/breadcrumb/issues/35
  (defun breadcrumb-org-crumbs ()
    "Get the chain from the top level heading down to current heading."
    (org-format-outline-path (org-get-outline-path t)
                             (1- (frame-width))
                             nil
                             " > "))
  (defun breadcrumb--header-line ()
    "Helper for `breadcrumb-headerline-mode'."
    (let* ((imenu-crumbs (if (eq major-mode 'org-mode)
                             'breadcrumb-org-crumbs
                           'breadcrumb-imenu-crumbs))
           (x (cl-remove-if
               #'seq-empty-p (mapcar #'funcall
                                     `(breadcrumb-project-crumbs ,imenu-crumbs)))))
      (mapconcat #'identity x (propertize " : " 'face 'breadcrumb-face))))

  (breadcrumb-mode))

Dark Theme for GTK Emacs

I prefer dark gtk widgets like tool bar and menu bar. They keeps Emacs dark theme really dark, and still looks good in Emacs light theme.

To set it, put the following lines into ~/.config/gtk-3.0/settings.ini.

[Settings]
gtk-application-prefer-dark-theme=1
gtk-icon-theme-name = Adwaita
gtk-theme-name = Adwaita
gtk-key-theme-name = Emacs

Cleaner Look

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

;; On Emacs without X, tool-bar-mode and scroll-bar-mode are not defined.
(when (fboundp 'tool-bar-mode)
  (tool-bar-mode -1))

(when (fboundp 'scroll-bar-mode)
  (scroll-bar-mode -1))

;; Show column number in the mode line.
(column-number-mode)

Startup Screen

Inhibit the startup screen and shorten the scratch buffer’s message.

(setq inhibit-startup-screen t)
(setq initial-scratch-message ";; scratch buffer\n\n")

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

Most of the time, I have only 1 or 2 windows in a frame. Rarely 3, and probably never 4 or above. other-window just works.

I bind it to M-o in additional to the default C-x o for even faster navigation.

(global-set-key (kbd "M-o") #'other-window)

I had tried windmove and ace-window but disabled both because other-window just work.

(use-package windmove
  :bind
  ("C-M-<up>" . 'windmove-up)
  ("C-M-<down>" . 'windmove-down)
  ("C-M-<left>" . 'windmove-left)
  ("C-M-<right>" . 'windmove-right))

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

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 "<f10>") #'my/toggle-current-window-dedication)

Desktop Save Mode

;; starting Emacs from a directory where you have a saved desktop configuration
;; will restore that configuration.
(setq desktop-path `(,user-emacs-directory))

;; do not load desktop if it is being used.
(setq desktop-load-locked-desktop nil)

;; Set how much buffer to restore immediately. Remaining buffers are restore
;; lazily when Emacs is idle.
;; (setq desktop-restore-eager 0)

;; Disables because desktop-save-mode
;; - keeps too many buffers in memory
;; - slows down start up graduately
;; (desktop-save-mode 1)

Tab Bar Mode

I use the tab bar mode to keep important buffers visible. For example, I create a new tab for a frequently used buffer so that I can switch to it real quick.

Note that it is common to use tab bar mode with desktop mode to restore the window configuration. However, I am not using that.

(setq tab-bar-close-button-show nil)
(setq tab-bar-new-button-show nil)

;; hide the tab bar when only one tab. Looks better especially when editting a file with emacsclient.
(setq tab-bar-show 1)
;; Show the absolute number of the tab
(setq tab-bar-tab-hints t)
;; Switch to tab by C-?
(setq tab-bar-select-tab-modifiers '(control))

(global-set-key (kbd "C-<tab>") #'tab-next)

(tab-bar-rename-tab "org" 1)

(tab-bar-mode +1)

Focus to the New Window after Splitting

I found it is more nature to focus to the new window after splitting.

(defun my/split-window-advice (&rest args) (other-window 1))
(advice-add 'split-window-below :after #'my/split-window-advice)
(advice-add 'split-window-right :after #'my/split-window-advice)

Prefer Splitting Horizontally with At Most 2 Windows

My preferred Emacs frame layout is either:

This allows a predictable behavior for other-window.

;; When nil, `split-window-sensibly' is not allowed to split window vertically.
;; e.g., when going to the heading from org-agenda.
(setq split-height-threshold nil)

;; Update the `split-width-threshold' so that it splits at most two
;; windows in a frame horizontally. It has to be a value
;; - <= (`frame-width' * 1.00) so 1 window will split
;; - <= (`frame-width' * 0.67) so 1 window will split with side window
;; - >  (`frame-width' * 0.50) so 2 windows won't split.
(defun my/update-split-width-threshold ()
  (setq split-width-threshold (round (* (frame-width) 0.6))))

;; Update the `split-width-threshold' whenever the window configuration is
;; changed.
(add-hook 'window-configuration-change-hook #'my/update-split-width-threshold)

Completion & Discoverability

Completion with vertico

(use-package vertico
  :init
  (vertico-mode)
  (setq vertico-count 25)
  :bind
  (:map vertico-map
        ([escape] . minibuffer-keyboard-quit)
        ("<prior>" . vertico-scroll-down)
        ("<next>" . vertico-scroll-up)))

;; Persist history over Emacs restarts. Vertico sorts by history position.
(use-package savehist
  :init
  (savehist-mode))

;; A few more useful configurations...
(use-package emacs
  :init
  ;; Add prompt indicator to `completing-read-multiple'.
  ;; We display [CRM<separator>], e.g., [CRM,] if the separator is a comma.
  (defun crm-indicator (args)
    (cons (format "[CRM%s] %s"
                  (replace-regexp-in-string
                   "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" ""
                   crm-separator)
                  (car args))
          (cdr args)))
  (advice-add #'completing-read-multiple :filter-args #'crm-indicator)

  ;; Do not allow the cursor in the minibuffer prompt
  (setq minibuffer-prompt-properties
        '(read-only t cursor-intangible t face minibuffer-prompt))
  (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)

  ;; Emacs 28: Hide commands in M-x which do not work in the current mode.
  ;; Vertico commands are hidden in normal buffers.
  (setq read-extended-command-predicate
        #'command-completion-default-include-p)

  ;; Enable recursive minibuffers
  (setq enable-recursive-minibuffers t))

;; Optionally use the `orderless' completion style.
(use-package orderless
  :init
  ;; Configure a custom style dispatcher (see the Consult wiki)
  ;; (setq orderless-style-dispatchers '(+orderless-dispatch)
  ;;       orderless-component-separator #'orderless-escapable-split-on-space)
  (setq completion-styles '(orderless basic)
        completion-category-defaults nil
        completion-category-overrides '((file (styles partial-completion)))))

Consult

(use-package consult
  :bind
  ("C-f" . consult-line)
  ("C-j" . consult-imenu)
  ("C-o" . consult-outline)
  ("C-r" . consult-recent-file)
  ("C-x r r" . consult-bookmark)

  ("C-x b" . consult-buffer)                ;; orig. switch-to-buffer
  ("C-x p b" . consult-project-buffer)      ;; orig. project-switch-to-buffer
  ("M-y" . consult-yank-pop)                ;; orig. yank-pop
  ("M-g g" . consult-goto-line)             ;; orig. goto-line
  :init
  (with-eval-after-load "org"
    (define-key org-mode-map (kbd "C-j") #'consult-org-heading)))

Embark & Marginalia

(use-package marginalia
  :init
  (setq marginalia-align 'center)
  (marginalia-mode))

(use-package embark
  :bind
  (("C->" . embark-act)         ;; pick some comfortable binding
   ("C-h B" . embark-bindings)) ;; alternative for "describe-bindings"

  :init
  ;; Optionally replace the key help with a completing-read interface
  (setq prefix-help-command #'embark-prefix-help-command)

  :config
  ;; Hide the mode line of the Embark live/completions buffers
  (add-to-list 'display-buffer-alist
               '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*"
                 nil
                 (window-parameters (mode-line-format . none)))))

;; Consult users will also want the embark-consult package.
(use-package embark-consult
  :hook
  (embark-collect-mode . consult-preview-at-point-mode))

which-key

which-key displays the key bindings following your currently entered incomplete command (a prefix).

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

Hydra Consult Menu

Create a hydra menu for the consult commands I use frequently.

(use-package hydra
  :commands defhydra
  :init
  (defhydra hydra-consult-menu (:hint nil
                                      :foreign-keys warn
                                      :exit t
                                      :pre (setq which-key-inhibit t)
                                      :post (setq which-key-inhibit nil))
    "===== Consult Menu (F12) =====\n"
    ("f" (my/consult-org-agenda) "Agenda")
    ("r" (consult-recent-file) "Recentf")
    ("q" nil "quit menu" :color blue))
  (global-set-key (kbd "<f8>") #'hydra-consult-menu/body))

Hydra File Menu

A hydra menu to open common files.

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

(use-package hydra
  :commands defhydra
  :init
  (defhydra hydra-file-menu (:hint nil
                                   :foreign-keys warn
                                   :exit t
                                   :pre (setq which-key-inhibit t)
                                   :post (setq which-key-inhibit nil))
    ("er" (load-file user-init-file))
    ("ed" (let ((vc-follow-symlinks t)) (find-file (locate-user-emacs-file "dotemacs.org"))) "dotemacs.org")
    ("ep" (let ((vc-follow-symlinks t)) (find-file (locate-user-emacs-file "private.org"))) "private.org")
    ("eg" (let ((vc-follow-symlinks t)) (find-file (locate-user-emacs-file "google.org"))) "google.org")
    ("ef" (let ((vc-follow-symlinks t)) (find-file (locate-user-emacs-file "elfeed.org"))) "elfeed.org")

    ("i" (find-file (my/org-inbox-file)) "inbox.org")
    ("p" (find-file (my/org-tasks-file)) "tasks.org")
    ("r" (find-file (my/org-references-file)) "references.org")
    ("d" (find-file (my/org-diary-file)) "diary.org")
    ("b" (find-file (my/org-blog-file)) "blog.org")

    ("w" (find-file (my/org-work-file) "work.org"))
    ("D" (find-file "~/org/work/diary.org") "work/diary.org")

    ("q" nil "quit menu" :color blue))

  (global-set-key (kbd "C-c e") #'hydra-file-menu/body))

Hydra Snippet Menu

Very often, I want to insert today’s date. There are many ways of doing this.

(defun my/current-date ()
  (concat "[" (shell-command-to-string "echo -n $(date '+%Y-%m-%d %a')") "]"))

;; NOTE: Consider using "org-time-stamp-inactive" if there is a way to
;; avoid the date selection prompt.
(defun my/insert-current-date ()
  (interactive)
  (insert (my/current-date)))

(use-package hydra
  :commands defhydra
  :init
  (defhydra my/hydra-snippets (:hint nil
                                     :foreign-keys warn
                                     :exit t)
    "Snippets"
    ("d" (my/insert-current-date) "Current Date (yyyy-mm-dd Aaa)")
    ("q" nil "Quit Menu"))

  (global-set-key (kbd "C-c s") #'my/hydra-snippets/body))

Org-mode

Using Emacs Org-Mode for note taking is not a solution for everyone. It comes with some drawbacks:

Despite these, I prefer Emacs Org-Mode for its PKM capabilities :-)

Also watch https://youtu.be/Bpmkeh4D98s?t=1204 which discusses the “Maximal” (tasks and notes in Emacs) and “Minimal” (tasks in Emacs, notes in other app) approach to Org mode.

Org Files

I started my #orgmode journey with #orgroam (2020-2022) and later #denote (2022-2023), mostly because of the preconception of using many small notes is the norm.

As I got cozy with Org mode and more comfortable working with large org files, I moved to a few big Org files.

By putting related notes next to each other, they are linked without explicit links.

With consult-org-heading, I can jump to any notes by heading. I don’t even need the SQL nor the file naming schema dependency.

I split org files when it becomes too big and causing performance issue.

(require 'org)

;; Adding a "/" so that =find-file= finds the files under =~/org/=.
(setq org-directory "~/org/")
(defun my/expand-org-file-name (filename)
  (expand-file-name filename org-directory))

(defun my/org-inbox-file () (my/expand-org-file-name "inbox.org"))
(defun my/org-tasks-file () (my/expand-org-file-name "tasks.org"))
(defun my/org-references-file () (my/expand-org-file-name "references.org"))
(defun my/org-diary-file () (my/expand-org-file-name "diary.org"))
(defun my/org-blog-file () (my/expand-org-file-name "blog.org"))
(defun my/org-work-file () (my/expand-org-file-name "work.org"))
(defun my/org-drill-file () (my/expand-org-file-name "drill.org"))
(defun my/org-calendar-directory () (my/expand-org-file-name ".calendar/"))
(defun my/org-attachment-directory () (my/expand-org-file-name ".attachment/"))
(defun my/org-emacs-config-file () (expand-file-name "dotemacs.org" user-emacs-directory))

Capturing

(global-set-key (kbd "C-c c") #'org-capture)

;; The default file for capturing.
(setq org-default-notes-file (my/org-inbox-file))

;; Org Capture Templates
;;
;; See https://orgmode.org/manual/Template-elements.html#index-org_002ddefault_002dnotes_002dfile-1
(setq org-capture-templates nil)
(add-to-list
 'org-capture-templates
 `("i" "Inbox" entry (file+headline ,(my/org-tasks-file) "Inbox")
   "* %?\n%i\n%a"))

(add-to-list
 'org-capture-templates
 `("I" "Inbox (Work)" entry (file+headline ,(my/org-work-file) "Inbox")
   "* %?\n%i\n%a"))

(add-to-list
 'org-capture-templates
 `("p" "Project" entry (file+headline ,(my/org-tasks-file) "Projects")
   (file "~/org/.template/project.org")))

(add-to-list
 'org-capture-templates
 `("v" "Vocab" entry (file+headline ,(my/org-drill-file) "Translation")
   "* %? :drill:\n\n** Translation\n\n** Definition\n"))

(use-package org-cliplink)

Attachment

I use the built-in org-attach for attachment and put all attachments under the same org-attach-id-dir by default. If necessary, use .dir-locals.el or the :DIR: property to customize org-attach-id-dir.

(require 'org-attach)
(setq org-attach-id-dir (my/org-attachment-directory))
(setq org-attach-use-inheritance t)

;; Use timestamp as ID and attachment folder. See https://helpdeskheadesk.net/2022-03-13/
;; (setq org-id-method 'ts)
;; (setq org-attach-id-to-path-function-list
;;       '(org-attach-id-ts-folder-format
;;         org-attach-id-uuid-folder-format))
;; Shorten the Org timestamp ID
;; (setq org-id-ts-format "%Y%m%dT%H%M%S")

Alternatives:

Task Management

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

(setq org-todo-keywords
      '((sequence "TODO(t)" "NEXT(n)" "PROG(p)" "|" "DONE(d)" "CANCELED(c)")))

(global-set-key (kbd "C-c a") #'org-agenda)

;; https://orgmode.org/worg/org-tutorials/org-custom-agenda-commands.html
(setq org-agenda-custom-commands
      '(("n" "Agenda / PROG / TODO"
         ((agenda "" nil)
          (tags "INBOX+LEVEL=2|CATEGORY=\"Inbox\"+LEVEL=1")
          (todo "PROG" nil)
          (todo "NEXT" nil))
         nil)))

;; My agenda files.
(setq org-agenda-files (list (my/org-inbox-file) (my/org-tasks-file) (my/org-calendar-directory)))

;; Trying to use the current window as agenda frame.
(setq org-agenda-window-setup 'current-window)

;; Use sticky agenda since I need different agenda views (personal and work) at the same time.
(setq org-agenda-sticky t)

;; Just today
(setq org-agenda-span 'day)

;; Hide all scheduled todo.
(setq org-agenda-todo-ignore-scheduled 'all)

;; Ignores "far" deadline TODO items from TODO list.
(setq org-agenda-todo-ignore-deadlines 'far)

;; Hide all scheduled todo, from tags search view, like tags-todo.
(setq org-agenda-tags-todo-honor-ignore-options t)

;; Hide all done todo in agenda
(setq org-agenda-skip-scheduled-if-done t)

;; Hide task until the scheduled date.
(setq org-agenda-skip-deadline-prewarning-if-scheduled 'pre-scheduled)


;; Use an indirect buffer after <Tab> (org-agenda-goto) or <Enter> (org-agenda-switch-to).
;;
;; Also see https://emacs.stackexchange.com/a/17822
;; (advice-add 'org-agenda-goto :after
;;             (lambda (&rest args)
;;               (org-tree-to-indirect-buffer)))
;; (advice-add 'org-agenda-switch-to :after
;;             (lambda (&rest args)
;;               (org-tree-to-indirect-buffer)))

;; Narrow to subtree after <Tab> (org-agenda-goto) or <Enter> (org-agenda-switch-to).
;; (advice-add 'org-agenda-goto :after
;;             (lambda (&rest args)
;;               (org-narrow-to-subtree)))
;; (advice-add 'org-agenda-switch-to :after
;;             (lambda (&rest args)
;;               (org-narrow-to-subtree)))

(advice-add 'org-agenda-goto :after
            (lambda (&rest args)
              (org-reveal)))
(advice-add 'org-agenda-switch-to :after
            (lambda (&rest args)
              (org-reveal)))

(setq org-enforce-todo-dependencies t)

(setq org-log-into-drawer t)
;; (setcdr (assoc 'note org-log-note-headings) "%d")
;; Interstitial Journaling: add note to CLOCK entry after clocking out
;; https://emacs.stackexchange.com/questions/37526/add-note-to-clock-entry-after-clocking-out
(setq org-log-note-clock-out t)

;; 4 priorities to model Eisenhower's matrix.
;; - [#A] means +important +urgent
;; - [#B] means +important -urgent
;; - [#C] means -important +urgent
;; - [#D] means -important -urgent
(setq org-priority-default 68
      org-priority-lowest 68)

;; Include the file name into the path in refile target.
;; Also see https://github.com/minad/vertico?tab=readme-ov-file#org-refile
(setq org-refile-use-outline-path 'file)
(setq org-outline-path-complete-in-steps nil)
(setq org-refile-targets `((nil :maxlevel . 2)))

In general,

I wrote some functions to customize the search scopes (e.g., which files to search).

(defun my/consult-org-agenda ()
  (interactive)
  (consult-org-agenda)
  (org-tree-to-indirect-buffer))

(setq my/consult-org-files '())
(add-to-list 'my/consult-org-files (my/org-inbox-file) t)
(add-to-list 'my/consult-org-files (my/org-tasks-file) t)
(add-to-list 'my/consult-org-files (my/org-references-file) t)
(when (file-exists-p (my/org-blog-file))
  (add-to-list 'my/consult-org-files (my/org-blog-file) t))
(when (file-exists-p (my/org-emacs-config-file))
  (add-to-list 'my/consult-org-files (my/org-emacs-config-file) t))

(defun my/consult-org-all ()
  (interactive)
  (consult-org-heading
   "+LEVEL<=3"
   my/consult-org-files))

(global-set-key (kbd "C-S-j") #'my/consult-org-all)
(global-set-key (kbd "<f1>") #'my/consult-org-all)

;; Full text search the whole org directory
(defun my/consult-ripgrep-org-directory ()
  (interactive)
  (require 'consult)
  ;; Add "--no-ignore-vcs" to the rg command so todo.org could be searched.
  (let ((consult-ripgrep-args (concat consult-ripgrep-args " --no-ignore-vcs")))
    (consult-ripgrep org-directory "")))

(global-set-key (kbd "C-S-f") #'my/consult-ripgrep-org-directory)

Jump to a Random Org Heading

When organizing all notes in a giant org file, it is useful to review them with this function.

(defun my/org-random-heading ()
  "Jump to a random org heading in the current org file."
  (interactive)
  (goto-char (point-min))
  (let ((headings '()))
    (while (re-search-forward "^\\*+ " nil t)
      (push (point) headings))
    (when headings
      (goto-char (nth (random (length headings)) headings))
      (org-reveal))))

(with-eval-after-load "org"
  (define-key org-mode-map (kbd "C-c r") #'my/org-random-heading))

Quick Toggle Narrowing

I always toggle narrowing to subtree in big org files. I bind org-toggle-narrow-to-subtree to C-x n n because

  1. it is closes to other narrowing commands with prefix C-x n, and
  2. it is easy to type as it repeats the last key n.
(define-key org-mode-map (kbd "C-x n n") #'org-toggle-narrow-to-subtree)

Pulling Google Calendar

I followed https://orgmode.org/worg/org-tutorials/org-google-sync.html to sync Google Calendar to org but not the other way, including a cron job every hour. Also, here is a elisp command to fetch manually.

(defun my/sync-calendar()
  (interactive)
  (message "Synchronizing...")
  (shell-command "~/org/.calendar/fetch.sh")
  (message "Synchronized."))

Literate Programming with Org-Babel

https://orgmode.org/worg/org-contrib/babel/languages/index.html

(org-babel-do-load-languages
 'org-babel-load-languages
 '((emacs-lisp . t) (shell . t) (python . t) (gnuplot . t) (ditaa . t)))

(setq org-edit-src-content-indentation 0)

;; Silent result by default. Append to override the default value.
(add-to-list 'org-babel-default-header-args '(:results . "silent") t)

;; Use python3.
(setq org-babel-python-command "python3")

There are drawback of using literate programming (Org-Babel), discussed in this reddit post. For example, no auto-complete, LSP, and IDE style feature.

Tip: Org Edit Special

To enable company mode for Org Babel code, use org-edit-special (C-c ') to enter the org-edit-src-code mode.

Auto Tangle

I do not use (add-hook #'org-mode #'org-auto-tangle-mode) because it enables the “auto-tangle” minor mode for too many files.

Instead, I enables this minor mode only when needed:

  1. I add -*- eval: (org-auto-tangle-mode); -*- to the org file that wants auto-tangle.
  2. I set org-auto-tangle-default to t to enable auto-tangle in all Org buffers with the minor mode activated.
(use-package org-auto-tangle
  :defer t
  :config
  (setq org-auto-tangle-default t))

Respect Content When Inserting Heading

(setq org-insert-heading-respect-content t)

Also see https://www.n16f.net/blog/org-mode-headline-tips/.

Clocking

(setq org-clock-mode-line-total 'current)
(setq org-show-notification-timeout 3600)

;; Comment out since I forgot why I added it.
;; (org-clock-auto-clockout-insinuate)

;; Create an indirect buffer for the current clocking task, and focus into it.
(defun my/org-clock-goto ()
  (interactive)
  (org-clock-goto)
  (org-tree-to-indirect-buffer)
  (other-window 1)
  (delete-other-windows))
(global-set-key (kbd "C-c C-x j") #'my/org-clock-goto)

Startup Appearances

I know there are packages to make Org Mode looks better, like org-bullets and org-modern. I am not using them. The org-indent-mode makes the layout nice and clear enough to me.

;; Enable the =org-indent-mode= by default.
(setq org-startup-indented t)

;; Set #+STARTUP: fold by default.
(setq org-startup-folded t)

;; Reduce org-indent-indentation-per-level from 2 to 1.
;;
;; This keeps =org-tags-column= the same for all headings.
;; Avoid inconsistency when eidting outside Emacs, like Beorg.
;; Note that Orgzly has option the customize this variable.
;; (setq org-indent-indentation-per-level 2)

Images

;; Whether to display the inline image at startup.
(setq org-startup-with-inline-images nil)

;; Display the image roughly match the org-tags-column.
;;
;; To customize the width, use "#+ATTR_ORG: :width <new_width>".
(setq org-image-actual-width 600)

See https://orgmode.org/manual/Images.html for more details.

Inline Latex Fragment

This snippet customize LaTeX Fragments.

;; To enable preview on startup globally
(setq org-startup-with-latex-preview nil)

;; Slightly increase the size of the Latex fragment previews.
(setq org-format-latex-options (plist-put org-format-latex-options :scale 1.2))

;; Put the generated image to an absolute directory.
;;
;; A side effect is that all latex images from this shared directory
;; might be copied during exporting.
;; e.g., ox-hugo (https://github.com/kaushalmodi/ox-hugo/issues/723)
;; (setq org-preview-latex-image-directory
;;       (expand-file-name "preview/ltximg/" user-emacs-directory))

Note that this requires installing Latex. For MacOS, install MacTex with brew install --cask mactex.

Pretty Entities

It renders entities as UTF-8 by default. e.g. π, Γ, ∑.

;; 2023-10-13: I prefer using M-x org-toggle-pretty-entities instead.
;; (setq org-pretty-entities t)

Plot Graph with gnuplot

(use-package gnuplot)

See https://github.com/dryman/org-math for the setup and usage.

Preview with org-preview-html

Open an Org file and execute M-x org-preview-html-mode. A preview window opens next to the Org file. See the repo for customization.

(use-package org-preview-html)

Disable Org Cache

When org element cache is broken for me (causing org-export to run forever),

;; (setq org-element-use-cache nil)

Blogging with ox-hugo

My blog is created using Hugo and ox-hugo. It generates better markdown than what you would get using org-md-export!

It works well out-of-the-box. However, extra configuration is required to embed video.

(use-package ox-hugo
  :after ox
  :config
  ;; By default, export to the /content/.
  (setq org-hugo-section "")
  (add-to-list 'org-hugo-external-file-extensions-allowed-for-copying "webm"))

Exporting Notes

;; Do not export with TOC, e.g., with org-md-export-as-markdown, org-ascii-export-as-ascii
(setq org-export-with-toc nil)

;; (setq org-export-with-section-numbers nil)

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
  :config
  ;; save buffers after drill sessions without prompt.
  (setq org-drill-save-buffers-after-drill-sessions-p nil)
  ;; reduce from the default 30 to make it to become a habit.
  (setq org-drill-maximum-items-per-session 10)
  :bind
  ("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.

Reserve “C-c <arrow>” for ‘windmove

(dolist (arrow '("left", "right", "up", "down"))
  (define-key org-mode-map (kbd (format "C-c <%s>" arrow)) nil t))

Reading

Google Translate

(use-package google-translate
  :init
  (setq google-translate-default-source-language "en")
  (setq google-translate-default-target-language "zh-TW")
  (setq google-translate-output-destination nil)
  :bind
  ("C-c v" . google-translate-at-point))

Writing

Spell Checking with flyspell-correct-wrapper

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

Synonyms

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

Write Good

The first challenge from Dreyer’s English is to write without weasel words.

(use-package writegood-mode
  :config
  (setq writegood-weasel-words
        '("very" "rather" "really" "quite" "in fact" "just" "so" "pretty" "of course" "surely" "that said" "actually")))

Auth Source

I use auth-source to manage secrets, mainly for api keys.

(require 'auth-source)

(add-to-list 'auth-sources (expand-file-name ".authinfo" user-emacs-directory))

LLM

LLM with llm

(use-package llm
  ;; :vc (llm :url "git@github.com:whhone/llm.git" :branch "streaming")
  ;; :vc (:fetcher github :repo ahyatt/llm)
  :init
  (setq llm-warn-on-nonfree nil)
  (require 'llm-openai)
  (setq my/llm-provider-openai
        (make-llm-openai :key (auth-source-pick-first-password :host "api.openai.com")))
  (require 'llm-gemini)
  (setq my/llm-provider-gemini
        (make-llm-gemini :key (auth-source-pick-first-password :host "gemini")))

  (setq
   my/llm-providers
   `(("Gemini" . ,my/llm-provider-gemini)
     ("Open AI" . ,my/llm-provider-openai)))

  (defun my/consult-llm-switch-provider ()
    "Switch the LLM provider."
    (interactive)
    (require 'consult)
    (let ((selected (consult--read
                     (mapcar #'car my/llm-providers)
                     :prompt "Switch my LLM provider: "
                     :require-match t)))
      (message "Switched LLM provider to '%s'" selected)
      (setq my/llm-provider (cdr (assoc selected my/llm-providers)))))

  (setq my/llm-provider my/llm-provider-gemini))

consult-llm Core

(defun my/get-region-string ()
  (unless (region-active-p)
    (user-error "No region active"))
  (buffer-substring (region-beginning) (region-end)))

(defun my/llm-buffer (provider prompt)
  (let ((prompt-info-heading (llm-chat-prompt-to-text prompt)))
    (display-buffer (get-buffer-create "*llm*"))
    (with-current-buffer (get-buffer-create "*llm*")
      (visual-line-mode +1)
      (erase-buffer)
      (insert prompt-info-heading)
      (insert "==== THINKING ====\n\n"))
    (llm-chat-streaming
     provider
     prompt
     `(lambda (response)
        (message "partial")
        (with-current-buffer (get-buffer-create "*llm*")
          (erase-buffer)
          (insert ,prompt-info-heading)
          (insert "==== PARTIAL ====\n\n")
          (insert response)))
     `(lambda (response)
        (message "full")
        (with-current-buffer (get-buffer-create "*llm*")
          (erase-buffer)
          (insert ,prompt-info-heading)
          (insert "==== FULL ====\n\n")
          (insert response)
          (delete-trailing-whitespace)))
     (lambda (err msg)
       (message "error")
       (with-current-buffer (get-buffer-create "*llm*")
         (insert ,prompt-info-heading)
         (insert "==== ERROR ==== %S %S" err msg))))))

(defun my/llm-region-with-header (header)
  (interactive "sHeader: ")
  (when (region-active-p)
    (my/llm-buffer
     my/llm-provider
     (make-llm-chat-prompt
      :interactions
      (list
       ;; Note: adding 'system break Gemini.
       ;; (make-llm-chat-prompt-interaction
       ;;  :role 'system
       ;;  :content "Be brief.")
       (make-llm-chat-prompt-interaction
        :role 'user
        :content (concat header "\n\n" (my/get-region-string))))))))

(defun my/llm-org-heading-with-header (header)
  (interactive "sHeader: ")
  (my/llm-buffer
   my/llm-provider
   (llm-make-simple-chat-prompt
    (concat header "\n\n" (my/org-breadcrumbs)))))

;; Explaining

(defun my/llm-explain-org-heading ()
  (interactive)
  (my/llm-org-heading-with-header
   "Explain the given topic briefly and provides 3 fun facts to help me remembering and learning it:"))

;; Writing

(defun my/llm-write-org-heading ()
  (interactive)
  (my/llm-org-heading-with-header
   "Write an article for the topic briefly in Markdown:"))

consult-llm Prompts

(setq my/llm-headers '())
;; When selected, `my/consult-llm' will ask for the header.
(add-to-list 'my/llm-headers '("*Custom*" . nil))

(add-to-list 'my/llm-headers '("Proofread" . "Proofread the text. Highlight the differences:"))
(add-to-list 'my/llm-headers '("Rewrite Funny" . "Rewrite the text in a funny manner:"))
(add-to-list 'my/llm-headers '("Rewrite Formal" . "Rewrite the text formally and keep it easy to read and understand:"))
(add-to-list 'my/llm-headers '("Rewrite Simple" . "Rewrite the text so that it is easy to read and understand, in complete sentences:"))
(add-to-list 'my/llm-headers '("Translate to English" . "Translate to English:"))
(add-to-list 'my/llm-headers '("Translate to Chinese" . "Translate to Traditional Chinese:"))
(add-to-list 'my/llm-headers '("Explain with Fun Facts" . "Explain the given topic briefly and provides 3 fun facts to help me remembering and learning it:"))
(add-to-list 'my/llm-headers '("Summerize" . "Summerize the text briefly and list the key highlights:"))
(add-to-list 'my/llm-headers '("Code Explain" . "Please explain the following code:"))
(add-to-list 'my/llm-headers '("Code Refactor" . "Please help me refactor the following code.
   Please reply with the refactoring explanation in English, refactored code, and diff between two versions.
   Please ignore the comments and strings in the code during the refactoring.
   If the code remains unchanged after refactoring, please say 'No need to refactor'."))

(defun my/consult-llm ()
  "Consult llm.

If there is an active region, then act on that. Otherwise, ask the user for the prompt."
  (interactive)
  (let ((llm--name (llm-name my/llm-provider)))
    (if (not (region-active-p))
        (my/llm-buffer
         my/llm-provider
         (llm-make-simple-chat-prompt
          (read-string
           (format "Enter prompt for %s: " llm--name))))
      (require 'consult)
      (let* ((selected (consult--read
                        (mapcar #'car my/llm-headers)
                        :prompt (format "Select header for %s: " llm--name)
                        :require-match t))
             (header (or (cdr (assoc selected my/llm-headers))
                         (read-string "Custom header: "))))
        (my/llm-region-with-header header)))))

(global-set-key (kbd "M-j") #'my/consult-llm)

Version Control

Diff Highlight with diff-hl

(use-package diff-hl
  ;; :vc (diff-hl :url "git@github.com:whhone/diff-hl.git" :branch "with-editor-fix")
  :init
  (global-diff-hl-mode)
  :config
  ;; Added in https://github.com/dgutov/diff-hl/pull/207
  (setq diff-hl-update-async t)

  ;; Disable async in `with-editor-mode'.
  ;; https://github.com/dgutov/diff-hl/issues/213
  (add-hook 'find-file-hook
            (lambda ()
              (when (bound-and-true-p with-editor-mode)
                (setq-local diff-hl-update-async nil))))

  (diff-hl-flydiff-mode +1)
  :hook
  (magit-pre-refresh . diff-hl-magit-pre-refresh)
  (magit-post-refresh . diff-hl-magit-post-refresh))

Magit

There are multiple key-binding to trigger magit:

C-x g
magit-status
C-c g
magit-dispatch
C-x f
magit-file-dispatch
C-x p m
The keybinding from project.el.

Other useful functions:

magit-log-*
shows the git log, e.g., magit-log-current, magit-log-buffer-file.

magit-rebase-interactive :

(use-package magit
  :init
  (setq magit-define-global-key-bindings 'recommended)

  ;; this binds `magit-project-status' to `project-prefix-map' when
  ;; project.el is loaded.
  (require 'magit-extras))

Project Management

Project

Note: Run project-forget-zombie-projects to remove deleted project directories.

project-prefix-map is bind to C-x p by default. (it seems no way to change it)

;; Use the entire "project-prefix-map" in "project-switch-project".
(setq project-switch-use-entire-map t)

Find File (a) in All Known Project

I particularly love the handy projectile function projectile-find-file-in-known-project and adding that function to project.el below.

(defun my/project-all-project-files ()
  "Get a list of all files in all known projects."
  (cl-mapcan
   (lambda (project)
     (when (file-exists-p project)
       (mapcar (lambda (file)
                 (expand-file-name file project))
               (project-files (project-current nil project)))))
   (project-known-project-roots)))

(defun my/project-find-file-in-known-projects ()
  "Find a file from all known projects."
  (interactive)
  (let* ((completion-ignore-case read-file-name-completion-ignore-case)
         ;; Note that "project--read-file-cpd-relative" is broken for
         ;; this function since Emacs 29. Instead of using
         ;; "project-read-file-name-function", always use
         ;; "project--read-file-absolute".
         (file (project--read-file-absolute
                "Find file in known projects" (my/project-all-project-files) nil nil
                (thing-at-point 'filename))))
    (if (string= file "")
        (user-error "You didn't specify the file")
      (find-file file))))

(define-key project-prefix-map (kbd "a") #'my/project-find-file-in-known-projects)

Full Text Search (g) in Current Project Files

(defun my/consult-ripgrep-current-project ()
  "Full text search with ripgrep in current project"
  (interactive)
  (consult-ripgrep (if (project-current) (project-root (project-current))
                     default-directory) ""))
(define-key project-prefix-map (kbd "g") #'my/consult-ripgrep-current-project)

Programming

Compilation

(setq compilation-scroll-output t)

(add-hook 'compilation-mode-hook #'goto-address-mode)

Terminal with vterm

(use-package vterm
  :config
  ;; Unset <f1> to <f12> in the `vterm-mode-map'
  (dotimes (fkey 12)
    (define-key vterm-mode-map (kbd (format "<f%d>" (1+ fkey))) nil))
  (define-key vterm-mode-map (kbd "C-p") nil)

  (setq vterm-shell (if (eq system-type 'darwin) "zsh" "bash"))
  (setq vterm-tramp-shells '(("docker" "/bin/sh")
                             ("ssh" "/bin/bash")
                             ("sshx" "/bin/bash")))
  :hook
  (vterm-mode . goto-address-mode)
  :bind
  ("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
          'executable-make-buffer-file-executable-if-script-p)

Kotlin Mode

(use-package kotlin-mode)

YAML Mode

(use-package yaml-mode)

Web Mode

https://web-mode.org/ is an autonomous major-mode for editing web templates.

(use-package web-mode
  :init
  (add-to-list 'auto-mode-alist '("\\.html\\'" . web-mode))

  ;; TODO: Use .dir.local because not all .html are go template
  (setq web-mode-engines-alist
        '(("go" . "\\.html\\'"))))

Markdown Mode

(use-package markdown-mode
  :bind
  (:map markdown-mode-map ("M-<left>" . markdown-promote))
  (:map markdown-mode-map ("M-<right>" . markdown-demote))
  (:map markdown-mode-map ("M-S-<left>" . markdown-promote-subtree))
  (:map markdown-mode-map ("M-S-<right>" . markdown-demote-subtree)))

Caddyfile Mode

(use-package caddyfile-mode
  :mode (("Caddyfile\\'" . caddyfile-mode)))

Company Mode

Another alternative is corfu. However, I have heard that it does not work well on terminal.

(use-package company
  :hook
  (prog-mode . company-mode)
  (eshell-mode . company-mode))

Eglot

(use-package eglot
  :config
  ;; (define-key eglot-mode-map
  ;;   [remap xref-find-definitions] #'eglot-find-typeDefinition)
  ;; (define-key eglot-mode-map
  ;;   [remap xref-find-references] #'eglot-find-declaration)
  (define-key eglot-mode-map (kbd "M-l r") 'eglot-rename)
  (define-key eglot-mode-map (kbd "M-l f") 'eglot-format)

  (add-hook 'c++-mode-hook #'eglot-ensure))

Eglot Booster

(use-package eglot-booster
  :if (executable-find "emacs-lsp-booster")
  :vc (:fetcher github :repo jdtsmith/eglot-booster)
  :after eglot
  :config (eglot-booster-mode))

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)

Tree Sitter

https://www.masteringemacs.org/article/how-to-get-started-tree-sitter

(setq treesit-language-source-alist
      '((bash "https://github.com/tree-sitter/tree-sitter-bash")
        (css "https://github.com/tree-sitter/tree-sitter-css")
        (html "https://github.com/tree-sitter/tree-sitter-html")
        (javascript "https://github.com/tree-sitter/tree-sitter-javascript" "master" "src")
        (json "https://github.com/tree-sitter/tree-sitter-json")
        (kotlin "https://github.com/fwcd/tree-sitter-kotlin")
        (make "https://github.com/alemuller/tree-sitter-make")
        (markdown "https://github.com/ikatyang/tree-sitter-markdown")
        (python "https://github.com/tree-sitter/tree-sitter-python")
        (toml "https://github.com/tree-sitter/tree-sitter-toml")
        (yaml "https://github.com/ikatyang/tree-sitter-yaml")))

;; Need to execute once.
(defun my/treesit-install-all-language-grammer ()
  (mapc #'treesit-install-language-grammar
        (mapcar #'car treesit-language-source-alist)))

(setq major-mode-remap-alist
      '((bash-mode . bash-ts-mode)
        (css-mode . css-ts-mode)
        (html-mode . html-ts-mode)
        (json-mode . json-ts-mode)
        ;; (java-mode . java-ts-mode)  ; built-in, but not quite ready.
        ;; (kotlin-mode . kotlin-ts-mode)
        (python-mode . python-ts-mode)
        (toml-mode . toml-ts-mode)
        (yaml-mode . yaml-ts-mode)))

Competitive Programming

Auto Insert the C++ Template

(setq auto-insert-directory (expand-file-name "auto-insert/" user-emacs-directory))

(define-auto-insert "\.cpp" "template.cpp")

;; Uncomment to enable auto-insert-mode globally. Otherwise, use "M-x
;; auto-insert" on demand.
;; (auto-insert-mode)

Compile and Run

See https://codeforces.com/blog/entry/101292.

(defun my/compile-and-run()
  (interactive)
  (if (eq major-mode 'c++-mode)
      (let* ((src (file-name-nondirectory (buffer-file-name)))
             (exe (file-name-sans-extension src)))
        (compile (concat "g++ -std=c++17 -O2 -Wall -Wno-sign-compare " src " -o /tmp/" exe " && time /tmp/" exe " < /tmp/input.txt" )))
    (recompile)))

(global-set-key (kbd "<f12>") #'my/compile-and-run)

Tramp

SSH Control Master Options

Update the tramp-ssh-controlmaster-options to match my .ssh/config so that it can reuse the existing SSH connection, if there is one.

Also see https://www.gnu.org/software/emacs/manual/html_node/tramp/Frequently-Asked-Questions.html.

(setq tramp-ssh-controlmaster-options
      (concat "-o ControlPath=~/.ssh/control-%%r@%%h:%%p "
              "-o ControlMaster=auto "
              "-o ControlPersist=yes"))

Make Less Verbose

I found Tramp could be spammy to the minibuffer when saving file. For example, these logs are printed to the minibuffer when saving a file.

Tramp: Encoding local file ‘/tmp/tramp.JniPQo.org’ using ‘base64-encode-region’...done
Tramp: Decoding remote file ‘/ssh:<user>@<host>:/path/to/test.org’ using ‘base64 -d -

Set tramp-verbose to 2 or lower to avoid showing the “connection to remote hosts” level message in minibuffer.

(setq tramp-verbose 2)

Chinese Support

Rime Input Method

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

Use C-\ (toggle-input-method) to toggle the input method. Run M-x rime-open-configuration to open the default.custom.yaml under rime-user-data-dir.

(use-package rime
  :init
  (when (eq system-type 'darwin)
    ;; Set the root for librime ("rime-api.h").
    ;; See https://github.com/DogLooksGood/emacs-rime/blob/master/INSTALLATION.org
    ;; 1. need to download a zip of rime for osx
    ;; 2. unzip it to ~/.emacs.d/librime/dist
    (setq rime-librime-root (expand-file-name "librime/dist" user-emacs-directory))

    ;; Set the root for emacs module ("emacs-module.h")
    ;; (setq rime-emacs-module-header-root "/opt/homebrew/opt/emacs-head@29/include/")
    (setq rime-emacs-module-header-root "/Applications/Emacs.app/Contents/Resources/include"))

  (setq default-input-method "rime")
  :custom
  (rime-show-candidate 'posframe)
  (rime-posframe-style 'vertical))

CJK Word Wrap

Emacs 28 adds better word wrap / line break support for CJK.

(setq word-wrap-by-category t)

GnuPG Passphrase Via Minibuffer

GnuPG could be used when signing git commit with GPG in Magit over the SSH.

See https://elpa.gnu.org/packages/pinentry.html for the setup steps. It requires adding allow-emacs-pinentry to the ~/.gnupg/gpg-agent.conf.

(use-package pinentry
  :init
  ;; Redirects all pinentry queries to the caller, so Emacs can query passphrase
  ;; through the minibuffer, instead of external pinentry program, like Gnome
  ;; Keyring. e..g, this makes plstore (org-gcal) asks password in minibuffer.
  (setq epg-pinentry-mode 'loopback)
  (pinentry-start))

Mac OS (Command: “M”, Option: “A”)

This preserves the Emacs’s bindings closer to Linux by setting the mac-*-modifier.

;; These are some key bindings ported from MacOS to everywhere, since
;; I found they are handy.

;; MacOS uses Command+f for "find".
(global-set-key (kbd "M-f") #'consult-line)
(global-set-key (kbd "M-F") #'my/consult-ripgrep-org-directory)

;; MacOS uses Command+s for "save".
(global-set-key (kbd "M-s") #'save-buffer)
(with-eval-after-load "org-agenda"
  (define-key org-agenda-mode-map (kbd "M-s") #'org-save-all-org-buffers))

;; MacOS uses Command+c to copy but I prefer keeping the Emacs style.
;; Disable it in case I mistrigger "capitalize-word".
(global-unset-key (kbd "M-c"))

(when (eq system-type 'darwin)
  ;; MacOS put the menu-bar in the top bar which already using the space.
  (menu-bar-mode 1)

  (setq mac-option-modifier 'alt)
  (setq mac-command-modifier 'meta)

  (global-set-key (kbd "<home>") #'move-beginning-of-line)
  (global-set-key (kbd "<end>") #'move-end-of-line)

  ;; MacOS uses Command+Control+f to toggle fullscreen. Also make it
  ;; work with Emacs.
  (global-set-key (kbd "C-M-f") #'toggle-frame-fullscreen)

  ;; MacOS uses Option+up/down to scroll.
  (global-set-key (kbd "A-<up>") #'scroll-down-command)
  (global-set-key (kbd "A-<down>") #'scroll-up-command)
  (global-set-key (kbd "A-<left>") #'move-beginning-of-line)
  (global-set-key (kbd "A-<right>") #'move-end-of-line)

  ;; (global-set-key (kbd "M-p") #'execute-extended-command)
  ;; (global-set-key (kbd "M-x") #'kill-region) ; already bind for s-x
  ;; sets fn-delete to be right-delete
  (global-set-key [kp-delete] #'delete-char)

  (defun my/apply-theme (appearance)
    "Load theme, taking current system APPEARANCE into consideration."
    ;; (mapc #'disable-theme custom-enabled-themes)
    (pcase appearance
      ('light (modus-themes-select 'modus-operandi))
      ('dark (modus-themes-select 'modus-vivendi-tinted))))

  (add-hook 'ns-system-appearance-change-functions #'my/apply-theme))

Personal Information

(setq user-mail-address "whhone@gmail.com"
      user-full-name "Wai Hon Law")

Setting exec-path to Search Programs to Run

By setting the exec-path, commands are available for Emacs (org babel, eshell, etc).

(defun my/add-to-exec-path (path)
  "Add `path' to `exec-path' if it exists."
  (when (file-exists-p path)
    (add-to-list 'exec-path path)))

(my/add-to-exec-path (expand-file-name "bin/proxy" (getenv "HOME")))
(my/add-to-exec-path (expand-file-name "Android/Sdk/platform-tools" (getenv "HOME")))
(my/add-to-exec-path (expand-file-name "bin" (getenv "HOME")))

HTML Rendering

(setq shr-max-width 80)
(setq shr-use-colors nil)

Load Private Config

(let ((vc-follow-symlinks t))
  (org-babel-load-file (concat user-emacs-directory "private.org")))