Emacs Config
This is my personal Emacs configuration, shared publicly and updated regularly. I hope it serves as a helpful resource for you.
Startup
Enable lexical binding
Lexical binding is a fundamental feature in Emacs Lisp that determines how variables are resolved, preferring lexical scope over dynamic scope. Enabling lexical binding is generally recommended as it improves code readability, maintainability, performance, and security.
To enable lexical binding for a specific Emacs Lisp file, add a file variable line near the beginning of the file that sets `lexical-binding` to `t`. A common and effective way is to make this the first line of the file, such as in your `init.el`:
;; -*- lexical-binding: t; -*-
Enhanced debug logging for --debug-init
(when init-file-debug
;; Helper to get time since initialization started
(defun my/time-from-init ()
(float-time
(time-subtract (current-time) before-init-time)))
;; Variables for tracking require calls
(setq my/debug-require-depth 0)
(setq my/debug-require-report '())
;; Advise 'require' to log calls with indentation and timing
(define-advice require
(:around (orig-func &rest args) logging)
(let ((pkg (nth 0 args)))
(let ((str
(format "%s[%.5f] require: %s"
(make-string my/debug-require-depth ?\s)
(my/time-from-init) pkg)))
(add-to-list 'my/debug-require-report str t)
(message str)))
(setq my/debug-require-depth (1+ my/debug-require-depth))
(apply orig-func args)
(setq my/debug-require-depth (1- my/debug-require-depth)))
;; Enable debug-on-error immediately
(toggle-debug-on-error)
;; Enable and report use-package statistics after init
(setq use-package-compute-statistics t)
(add-hook 'after-init-hook #'use-package-report))
Keep customizations separate
To avoid init.el
from modified by Custom, direct Emacs to save customizations to a separate file. Set the variable custom-file
and load the specified file. The following code snippet ensures the file exists before attempting to load it.
(setq custom-file (locate-user-emacs-file "custom.el"))
;; Create the custom file if it doesn't exist
(unless (file-exists-p custom-file)
(write-region "" nil custom-file))
;; Load the customizations from the file
(load custom-file)
Show the init time as startup message
(define-advice startup-echo-area-message
(:override (&rest args) emacs-init-time)
(format "Emacs init time: %s" (emacs-init-time)))
use-package - Defer loading
- Set
use-package-always-defer
to t.- It always take advantage of the autoload provided by the package.
- It provides consistent behavior (always defer) regardless of other autoload keywords like
:commands
,:bind
,:hook
, etc.
- To avoid deferring, use
:init
to call an autoload command, or:demand
.- Don’t use
:defer nil
because it can still defer there are other autoload keywords.
- Don’t use
- Use
:autoload
,:commands
,:hook
,:bind
, etc to set up additional autoloads. - Use
use-package-report
to understand which packages are loaded. See the--debug-init
section above.- For built-in packages/libraries, I still use
use-package
souse-package-report
include the time of those.
- For built-in packages/libraries, I still use
(use-package use-package
:config
(setq use-package-always-ensure t
use-package-always-defer t
use-package-hook-name-suffix nil
use-package-verbose t))
;; for :diminish support (hiding minor modes in mode line)
(use-package diminish)
gcmh - Garbage Collection
To reduce the startup time of Emacs, you can increase the garbage collection threshold to a higher value. See https://www.reddit.com/r/emacs/comments/3kqt6e. This reduced my startup time by half.
- To check the startup time, use
(emacs-init-time)
. - To check the number of garbage collections performed, see the value of
gcs-done
. - To print a message when garbage collections happen, set
garbage-collection-messages
to t.
gcmh
is a package to adjust the gc-cons-threshold
automatically. It increases the threshold when Emacs is in use to avoid GC. It reduces the threshold when Emacs is idle for 15s to let GC to happen.
(use-package gcmh
:diminish
:init (gcmh-mode 1))
Package Management
package – Add “melpa” to Package Archives
(use-package package
:config (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t))
Pin Packages to Specific Version
Pin the org
to the built-in version. The latest version might be incompatible.
I am not using the :pin
keyword from use-package
because it forces the package to load, and is also slow.
;; Note: the value ("built-in") could actually be any string that does
;; not mach an package archive in `package-archives'.
(setq package-pinned-packages '((org . "built-in")))
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
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".
(keymap-global-set "<escape>" #'keyboard-quit)
;; Avoid entering the ~repeat-complex-cammand~ when quiting everything with ~C-x~.
(keymap-global-unset "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-j" ; default-indent-new-line
"M-u" ; upcase-wrod
"C-z")) ; suspend-frame
(keymap-global-unset key))
;; Disable the "numeric argument". When used, I use the =C-u= prefix.
(dolist (prefix '("M-" "C-M-"))
(keymap-global-unset (concat prefix "-"))
(dotimes (i 10)
(keymap-global-unset (concat prefix (number-to-string i)))))
;; Disable the default binding for `compose-mail'.
(keymap-global-unset "C-x m")
display-line-numbers
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.
(use-package display-line-numbers
:hook (prog-mode-hook . display-line-numbers-mode))
display-fill-column-indicator
(use-package display-fill-column-indicator
:hook (prog-mode-hook . display-fill-column-indicator-mode))
hl-line
(use-package hl-line
; Highlight the current row for tabulated-list-mode and org-agenda-mode.
:hook ((prog-mode-hook tabulated-list-mode-hook org-agenda-mode-hook) . hl-line-mode))
goto-address
(use-package goto-addr
:hook (prog-mode-hook . goto-address-prog-mode)
:init (global-goto-address-mode 1))
paren
I type all paren by myself. I don’t use electric-pair-mode
which is too smart for me.
(use-package paren
:init (setq show-paren-delay 0.0))
visual-line-mode - “word-wrap”
(use-package emacs
:diminish visual-line-mode
:hook ((text-mode-hook help-mode-hook diff-mode-hook) . visual-line-mode))
truncate-lines
Give each line of text just one screen line in prog-mode
.
(defun my/truncate-lines ()
(setq-local truncate-lines t))
(add-hook 'prog-mode-hook #'my/truncate-lines)
Show Trailing Whitespace
Shows trailing whiteapces only for file visiting buffers.
(defun my/show-trailing-whitespace()
(setq-local show-trailing-whitespace t))
(add-hook 'find-file-hook #'my/show-trailing-whitespace)
When need to be more advanced, use whitespace-mode
or whitespace-newline-mode
to make whitespace more visible.
Better save-buffer
I made few improvements to save-buffer
:
- do not save for non-file-visiting buffer (See my blog post)
- clean up whitespace
(whitespace-cleanup)
- save asynchronously with thread to avoid blocking the UI
before-save-hook
does not work for (whitespace-cleanup)
:
- When the current line has trailing space.
- After saving the file (the trailing space is not removed, as expected).
- Move the cursor to the next line.
- Save the file again, the
before-save-hook
is not triggered because the file has not been altered from the last save.
(defvar my/save-buffer-with-thread nil
"If non-nil, execute save-buffer with thread so it does not block the UI")
(defvar my/save-buffer-with-whitespace-cleanup t)
(define-advice save-buffer
(:around (orig-func &rest args) with-thread-and-delete-trailing-whitespaces)
(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."))
;; Disable (whitespace-cleanup) when
;; - in diff-mode
;; - end-of-line and last char is a space (seems still editing)
(when (and my/save-buffer-with-whitespace-cleanup
(not (derived-mode-p 'diff-mode))
(not (eobp)) ; end-of-buffer
(not (and (eolp) ; end-of-line
(eq (char-after (1- (point))) 32))))
(whitespace-cleanup))
(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-func args)
(error
(message "Error from my/save-buffer-advice %S" err)
nil))
"my/save-buffer-advice")
(apply orig-func args)))
Auto Save and Auto Revert
Auto-save and auto-revert cause less conflict when editing files synchronized on multiple computers.
(use-package files
:ensure emacs
:hook (find-file-hook . auto-save-visited-mode)
:config
;; 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-default auto-save-visited-interval 2))
(use-package autorevert
:diminish auto-revert-mode
:hook (find-file-hook . auto-revert-mode)
:config
;; Uncomment to use a timer instead of file notification.
;; (setq auto-revert-use-notify nil)
;; (setq auto-revert-interval 2)
;; Watch and reload the file changed on the disk.
(setq auto-revert-remote-files t)
;; Do not generate any messages.
(setq auto-revert-verbose nil))
;; Do not create lock files (prefix ".#").
(setq create-lockfiles nil)
Revert All Buffers
auto-revert
might fail in some file systems or after waking up from sleep. If needed, I manually revert all buffers.
(defun my/revert-all-buffers ()
"Refreshes all open buffers from their respective files"
(interactive)
(let* ((list (buffer-list))
(buffer (car list)))
(while buffer
(when (and (buffer-file-name buffer)
(not (buffer-modified-p buffer)))
(set-buffer buffer)
(revert-buffer t t t))
(setq list (cdr list))
(setq buffer (car list))))
(message "Refreshed open files"))
Taken from https://blog.plover.com/prog/revert-all.html.
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)
delete-selection-mode - Delete the selected text before editing
(use-package delsel
:init (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)
;; Update the scroll amount with meta, from window height to 5.
(use-package mwheel
:ensure emacs
:demand
:config
(setcdr (assoc '(meta) mouse-wheel-scroll-amount) 5))
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)
(when (> (line-number-at-pos) 1)
(transpose-lines 1)
(previous-line 2)))
;; move line down
(defun my/move-line-down ()
(interactive)
(when (< (line-number-at-pos) (count-lines (point-min) (point-max)))
(next-line 1)
(transpose-lines 1)
(previous-line 1)))
(keymap-global-set "M-S-<up>" #'my/move-line-up)
(keymap-global-set "M-S-<down>" #'my/move-line-down)
- try drag-stuff.el
Kill the Current Lines
In Emacs, C-a C-k
is the keybinding to kill the current whole line. This trick also works in bash! If kill-whole-line
is non-null, kill the terminating newline. This behavior is usually what I prefer.
(setq kill-whole-line t)
Note that C-S-<backspace>
maps to a function kill-whole-line
that works in GUI but not terminal.
whole-line-or-region
whole-line-or-region brings some modern behaviors to Emacs:
- C-w
- kill the whole line if no region is selected
- M-w
- copy the whole line if no region is selected
For example, to replicate Vim’s dd
(delete line) then p
(paste the line). Do C-w
and then C-y
with whole-line-or-region
mode enabled.
(use-package whole-line-or-region
:diminish whole-line-or-region-local-mode
:init (whole-line-or-region-global-mode))
Comment Line
The default comment-line
keybinding C-x C-;
does not work in terminal. Adding C-x ;
for terminal.
(keymap-global-set "C-x ;" #'comment-line)
By default, comment-line
forwards one line afterwards. I add an advice to keep the point position.
(define-advice comment-line
(:around (orig-func &rest args) with-save-excursion)
"`comment-line' without forwarding 1 line."
(save-excursion (apply orig-func args)))
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)
;; In case tab is used for indentation, use 2 as the width.
(setq-default tab-width 2)
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))))))))
(keymap-global-set "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!")))
(keymap-global-set "C-x K" #'my/delete-file-and-buffer)
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.
(define-advice replace-string
(:around (orig-fun &rest args) with-case-fold-search-nil)
(let ((case-fold-search nil))
(apply orig-fun args)))
recentf - keep track of recently opened files
(use-package recentf
:demand
:config
;; =recentf-mode= startup latency is large when there is tramp file.
;; Exclude them by checking ":".
(add-to-list 'recentf-exclude ":")
(recentf-mode +1))
saveplace - automatically save place in files
;; Remember and restore the last cursor location of opened files
(use-package saveplace
:demand
:config
;; =save-place-mode= startup latency is large when there is tramp file.
;; Exclude them by checking ":".
(setq save-place-ignore-files-regexp (format ":\\|\\(%s\\)" save-place-ignore-files-regexp))
(save-place-mode 1))
wgrep
(use-package wgrep)
yasnippet - template system
Commented because I am not using it enough.
(use-package yasnippet
:bind
("C-c s i" . yas-insert-snippet)
("C-c s n" . yas-new-snippet)
:config
(define-advice yas-insert-snippet
(:before (&rest _) enable-yas-minor-mode)
"Ensure `yas-minor-mode' is enabled for yas command."
(unless yas-minor-mode
(yas-minor-mode))))
(use-package yasnippet-snippets
:after yasnippet
:demand)
Very often, I want to insert today’s date. There are many ways of doing this.
;; NOTE: Consider using (org-timestamp-inactive) if there is a way to
;; avoid the date selection prompt.
(defun my/insert-current-date ()
(interactive)
(insert
(concat "[" (shell-command-to-string "echo -n $(date '+%Y-%m-%d %a')") "]")))
(keymap-global-set "C-c s d" #'my/insert-current-date)
Miscellaneous
;; Use year/month/day
(setq calendar-date-style 'iso)
;; 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 Size | With Rescaled Font Sizes |
---|---|
![]() | ![]() |
Here are the fonts and scale factors I used on the right side above:
Type | Font | Scale Factors |
---|---|---|
Default | Hack | 1 |
CJK | WenQuanYi Micro Hei | 1.2 |
Emoji | Noto Color Emoji | 0.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, HiDPI
((eq system-type 'darwin)
(my/set-fonts
"Menlo" 140
"Hiragino Sans CNS" 1.2
"Apple Color Emoji" 0.9))
;; Default Linux
(t
(my/set-fonts
"Source Code Pro" 100
;; "Hack" 100
"WenQuanYi Micro Hei" 1.25
"Noto Color Emoji" 1.00)))
;; in case the font is not installed.
;; (set-face-attribute 'default nil :height 100)
;; (set-face-attribute 'default nil :height 140)
;; (set-face-attribute 'default nil :height 200)
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
:demand
:bind
("<f5>" . modus-themes-toggle)
("<f6>" . modus-themes-toggle)
:config
(setq modus-themes-to-toggle
;; (light, dark)
'(modus-operandi modus-vivendi-tinted))
(setq modus-themes-headings
'((agenda-date . (1.2))))
;; color override
(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)))
;; face override
(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))))
;; tab bar
`(tab-bar
((,c :foreground "#ffffff" :background "#0d0e1c"
:underline (:style line :position t :color ,border)))) ; bg-active
`(tab-bar-tab ((,c :foreground "#ffffff" :background "#4a4f69"))) ; bg-active
`(tab-b2ar-tab-inactive ((,c :foreground "#ffffff" :background "#0d0e1c"))) ; bg-main
;; 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))))
`(eshell-ls-directory
((,c :weight bold)))
`(eshell-ls-symlink
((,c :foreground ,cyan :weight bold :underline nil)))
)))
(add-hook 'modus-themes-after-load-theme-hook #'my/modus-themes-custom-faces)
(defun my/apply-theme (appearance)
"Load theme, taking current system APPEARANCE into consideration."
(pcase appearance
('light (modus-themes-select (nth 0 modus-themes-to-toggle)))
('dark (modus-themes-select (nth 1 modus-themes-to-toggle)))))
(when (eq system-type 'darwin)
(define-advice modus-themes-toggle
(:override (&rest args) sync-with-osx)
(shell-command "open -a /Users/whhone/Library/Mobile\\ Documents/com~apple~Automator/Documents/Toggle\\ Dark\\ Theme.app")))
(if (boundp 'ns-system-appearance)
(add-hook 'ns-system-appearance-change-functions #'my/apply-theme)
;; if 8pm ~ 6am, use 'dark them.
(my/apply-theme
(let ((hour (string-to-number (format-time-string "%H" (current-time)))))
(if (or (>= hour 20) (< hour 6)) 'dark 'light)))))
Note: commands to list the colors
list-colors-display
- Display names of Emacs defined colors, and show what they look like.
modus-theme-list-colors
- Display names of Modus theme defined colors, and show what they look like.
Note: https://github.com/LionyxML/auto-dark-emacs is a package to sync Emacs’s theme with the OS. (I haven’t tried it yet)
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) |
---|---|
![]() | ![]() |
(use-package tooltip
:ensure emacs
:init (tooltip-mode 1))
Breadcrumb
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
:hook (find-file-hook . breadcrumb-local-mode)
: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."
(unless (org-before-first-heading-p)
(org-format-outline-path (org-get-outline-path t)
(1- (frame-width)) ; width
nil ; prefix
" > "))) ; separator
(define-advice breadcrumb--header-line
(:override (&rest args) support-org-mode)
"Helper for `breadcrumb-headerline-mode'."
(let* ((imenu-crumbs (if (derived-mode-p '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)))))
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
(use-package emacs
:init
(menu-bar-mode -1)
(when (featurep 'scroll-bar)
(scroll-bar-mode -1))
(when (featurep 'tool-bar)
(tool-bar-mode -1))
(column-number-mode))
Startup Screen & Scratch Buffer
Inhibit the startup screen and shorten the scratch buffer’s message.
(setq inhibit-startup-screen t)
(setq initial-scratch-message ";; scratch buffer\n\n")
;; (setq initial-scratch-message ";; Run M-x lisp-interaction-mode for Lisp programming.\n\n")
;; (setq initial-major-mode 'fundamental-mode)
Cursor
Customize the cursor’s color using the cursor
face.
;; Set the cursor color to red.
(set-face-attribute 'cursor nil :background "#ff5f59")
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.
(defun my/other-window-backward ()
(interactive)
(other-window -1))
(keymap-global-set "M-o" #'other-window)
(keymap-global-set "M-i" #'my/other-window-backward)
I had tried windmove
and ace-window
but disabled both because other-window
just work.
(use-package windmove
:disabled
:bind
("C-M-<up>" . 'windmove-up)
("C-M-<down>" . 'windmove-down)
("C-M-<left>" . 'windmove-left)
("C-M-<right>" . 'windmove-right)
:config
(with-eval-after-load "org"
;; Reserve =C-c <arrow>= for ='windmove= in Org
(dolist (arrow '("left", "right", "up", "down"))
(keymap-unset org-mode-map (format "C-c <%s>" arrow)))))
(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 Resize
M-x windresize
, then use the arrow key to resize the current window intuitively.
(use-package windresize
:bind ("C-x =" . windresize)
:config
(setq windresize-modifiers
'((meta) ; select window
(meta control) ; move the up/left border (instead of bottom/right)
(meta shift) ; move window while keeping the width/height
(control)))) ; temporarily negate the increment value
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))))
(keymap-global-set "<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.
(use-package tab-bar
:bind ("C-<tab>" . tab-next)
:config
(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))
(tab-bar-select-tab 1)
(setq tab-bar-separator " ")
(setq tab-bar-auto-width t))
Improving Window Splitting
I did two improvements to window split
- focus to the new window
- re-balance the window width and height
(defun my/split-window-below ()
(interactive)
(split-window-below)
(unless (derived-mode-p 'exwm-mode)
(balance-windows (window-main-window))
(other-window 1)))
(defun my/split-window-right ()
(interactive)
(split-window-right)
(unless (derived-mode-p 'exwm-mode)
(balance-windows (window-main-window))
(other-window 1)))
(defun my/delete-window ()
(interactive)
(delete-window)
(balance-windows (window-main-window)))
(keymap-global-set "C-x 2" #'my/split-window-below)
(keymap-global-set "C-x 3" #'my/split-window-right)
(keymap-global-set "C-x 0" #'my/delete-window)
;; The commented code using advice can affect function that handle
;; "other-window" by itself like =org-todo=.
;; (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)
;; (advice-add 'delete-window :after #'balance-windows)
I have tried 3 ways for rebalancing windows:
(balance-windows)
(balance-windows-area)
(balance-windows (window-main-window))
(3) is used because it balance without changing the side windows.
Use |
and -
for Splitting Right and Below
The default C-x 2
and C-x 3
are hard to reason. Also add “-” and “|”.
(keymap-global-set "C-x -" #'my/split-window-below)
(keymap-global-set "C-x |" #'my/split-window-right)
Prefer Splitting Horizontally with At Most 2 Windows
To ensure predictable behavior for (other-window)
, my preferred Emacs frame layouts are either a single window or two windows side by side.
;; 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.55))))
;; Update the `split-width-threshold' whenever the window configuration is
;; changed.
(add-hook 'window-configuration-change-hook #'my/update-split-width-threshold)
Display Buffer Alist
(defun my/side-window-config
(condition direction &optional slot)
(let* ((config-slot (or slot 0))
;; 1/3 in large screen. 1/3 in small screen.
(config-width (if (> (frame-width) 240) 0.333 0.333)))
`(,condition
(display-buffer-in-side-window)
(side . ,direction)
(slot . ,config-slot)
(window-width . ,config-width)
(dedicated . t)
(window-parameters
.
(;; `other-window'. can switch to side window.
(no-other-window . nil)
;; `delete-other-windows' cannot delete side window.
(no-delete-other-windows . t))))))
(defun my/update-display-buffer-alist ()
(interactive)
(setq display-buffer-alist
`(;; right side window
,(my/side-window-config
'(or . ((derived-mode . help-mode)
(derived-mode . message-buffer-mode)
(derived-mode . magit-status-mode)
"COMMIT_EDITMSG" ; with-editor
"*Gemini*" ; gptel
"*xref*" ; xref
"fig: "))
'right)
;; bottom side window
,(my/side-window-config
'(or . ((derived-mode . compilation-mode)
(derived-mode . eshell-mode)
(derived-mode . shell-mode)
(derived-mode . term-mode)
"*vterm*"))
'bottom)
;; do not display this window
("*Async Shell Command*"
(display-buffer-no-window)))))
;; Update the `split-width-threshold' whenever the window
;; configuration is changed.
(add-hook 'window-configuration-change-hook
#'my/update-display-buffer-alist)
(keymap-global-set "M-`" #'window-toggle-side-windows)
;; for mac os
(keymap-global-set "M-0" #'window-toggle-side-windows)
(defun my/has-side-windows ()
"Whether the current frame has side windows showing."
(not (frame-root-window-p (window-main-window))))
(defun my/get-window-side ()
"Return the side of the current window."
(window-parameter (selected-window) 'window-side))
(define-advice display-buffer
(:before (&rest args) ensure-side-window-dedicated)
"If on a side window, ensure it is dedicated so that it won't be used by `display-buffer'."
;; e.g., Otherwise, `org-agenda' throws error "cannot make side window the only window"
(when (my/get-window-side)
(set-window-dedicated-p (selected-window) t)))
avy: Navigate to Visible Text
Experimenting this package.
(use-package avy
:bind ("M-g c" . avy-goto-char))
Completion & Discoverability
vertico / savehist / orderless
(use-package vertico
:bind
(:map vertico-map
([escape] . minibuffer-keyboard-quit)
("<prior>" . vertico-scroll-down)
("<next>" . vertico-scroll-up))
:init
(vertico-mode)
(setq vertico-count 25))
;; Persist history over Emacs restarts. Vertico sorts by history position.
(use-package savehist
:init (savehist-mode))
;; Add prompt indicator to `completing-read-multiple'.
;; We display [CRM<separator>], e.g., [CRM,] if the separator is a comma.
(define-advice completing-read-multiple
(:filter-args (args) vertico)
(cons (format "[CRM%s] %s"
(replace-regexp-in-string
"\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" ""
crm-separator)
(car args))
(cdr args)))
;; 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
C-j
- Local file search with
consult-imenu
andconsult-org-heading
of the current file. C-f
- Local full text search with
consult-line
. C-o
- Global org agenda heading search with
consult-org-agenda
. C-u C-o
- Global full text search for all org files with
consult-ripgrep
.
(use-package consult
:autoload (consult--read)
:bind
("C-f" . consult-line)
("C-j" . consult-imenu)
("C-o" . my/consult-org)
("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"
(keymap-set org-mode-map "C-j" #'consult-org-heading))
(with-eval-after-load "org-agenda"
(keymap-set org-agenda-mode-map "C-j" #'consult-org-agenda))
:config
(setq consult-line-start-from-top nil)
;; Full text search the whole org directory
(defun my/consult-org (arg)
"Consult the org agenda headings. If prefix is provided, performs full text search."
(interactive "P")
(if arg
;; "--no-ignore-vcs" : inbox.org can also be search.
;; "-t org" : only search for org file.
(let ((consult-ripgrep-args (concat consult-ripgrep-args " --no-ignore-vcs" " -t org")))
(consult-ripgrep org-directory ""))
(consult-org-agenda "LEVEL<=3"))))
;; Use 'consult-xref as the xref functions.
(use-package consult-xref
:ensure consult
:after xref
:init
(setq xref-show-xrefs-function #'consult-xref)
(setq xref-show-definitions-function #'consult-xref))
embark
(use-package embark
:bind
(("M-SPC" . 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-hook . consult-preview-at-point-mode))
embark: Use which-key like menu prompt
(defun embark-which-key-indicator ()
"An embark indicator that displays keymaps using which-key.
The which-key help message will show the type and value of the
current target followed by an ellipsis if there are further
targets."
(lambda (&optional keymap targets prefix)
(if (null keymap)
(which-key--hide-popup-ignore-command)
(which-key--show-keymap
(if (eq (plist-get (car targets) :type) 'embark-become)
"Become"
(format "Act on %s '%s'%s"
(plist-get (car targets) :type)
(embark--truncate-target (plist-get (car targets) :target))
(if (cdr targets) "…" "")))
(if prefix
(pcase (lookup-key keymap prefix 'accept-default)
((and (pred keymapp) km) km)
(_ (key-binding prefix 'accept-default)))
keymap)
nil nil t (lambda (binding)
(not (string-suffix-p "-argument" (cdr binding))))))))
(setq embark-indicators
'(embark-which-key-indicator
embark-highlight-indicator
embark-isearch-highlight-indicator))
(define-advice embark-completing-read-prompter
(:around (orig-func &rest args) embark-hide-which-key-indicator)
"Hide the which-key indicator immediately when using the completing-read prompter."
(which-key--hide-popup-ignore-command)
(let ((embark-indicators
(remq #'embark-which-key-indicator embark-indicators)))
(apply orig-func args)))
marginalia
(use-package marginalia
:init
(setq marginalia-align 'center)
(marginalia-mode))
which-key
which-key displays the key bindings following your currently entered incomplete command (a prefix). Note that which-key is going into Emacs 30!
(use-package which-key
:diminish
:init (which-key-mode +1))
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
: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")
("ex" (let ((vc-follow-symlinks t)) (find-file (locate-user-emacs-file "exwm.org"))) "exwm.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")
("sc" (my/sudo-find-file "/etc/caddy/conf.d/whhone.com.Caddyfile") "sudo:Caddyfile")
("sm" (my/sudo-find-file "/etc/miniflux.conf") "sudo:miniflux.conf")
("ss" (my/sudo-find-file "/etc/samba/smb.conf") "sudo:smb.conf")
("q" nil "quit menu" :color blue))
:bind ("C-c e" . hydra-file-menu/body))
Org-mode
Using Emacs Org-Mode for note taking is not a solution for everyone. It comes with some drawbacks:
- Configuration time required
- Limited mobile support
- Text format limitations (e.g., image support)
- Limited sharing and collaboration
Despite these, I prefer Emacs Org-Mode for its PKM capabilities :-)
- Most of the time, I am using a computer. I use mobile for capturing.
- Plain text is portable and future-proof, easy to self-hosted.
- Personal notes is personal. If collaboration is needed, I use other tools.
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
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.
See this blog post for my Org-Mode workflow for task management.
(use-package org
:init
;; fix for "Invalid function: org-element-with-disabled-cache"
;; https://www.reddit.com/r/emacs/comments/1hayavx/invalid_function_orgelementwithdisabledcache/
(setq native-comp-jit-compilation-deny-list '(".*org-element.*"))
;; 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))
(setq my/org-inbox-file (my/expand-org-file-name "inbox.org"))
(setq my/org-tasks-file (my/expand-org-file-name "tasks.org"))
(setq my/org-references-file (my/expand-org-file-name "references.org"))
(setq my/org-diary-file (my/expand-org-file-name "diary.org"))
(setq my/org-blog-file (my/expand-org-file-name "blog/ox/content.org"))
(setq my/org-work-file (my/expand-org-file-name "work.org"))
(setq my/org-drill-file (my/expand-org-file-name "drill.org"))
(setq my/org-attachment-directory (my/expand-org-file-name ".attachment/"))
:config
;; Note: WAIT(w) is an experimental state for short-term wait (~10
;; mins ~ few hours) within today so that I can switch to anther task
;; and come back later.
(setq org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "PROG(p)" "WAIT(w)" "|" "DONE(d)" "CANX(c)")
(sequence "ASK(a)" "|")))
(setq org-enforce-todo-dependencies 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))
org-agenda
(use-package org-agenda
:ensure org
:bind ("C-c a" . org-agenda)
:init (setq org-agenda-files (list my/org-inbox-file my/org-tasks-file))
:config
(defun my/org-agenda-daily-command (prefix name org-agenda-files)
"Helper function to generate daily agenda for the given agenda files."
`(,prefix
,name
((agenda
""
;; shows deadline in another section below
;; ((org-agenda-entry-types '(:scheduled :timestamp :sexp)))
)
(tags
"LEVEL<=2+CATEGORY={Inbox.*}-ITEM=\"Inbox\""
((org-agenda-overriding-header "Inbox")))
(tags-todo
"+PRIORITY=\"A\""
((org-agenda-overriding-header "High Priority")))
;; (agenda
;; ""
;; ((org-agenda-overriding-header "Deadlines in 7 Days")
;; (org-agenda-time-grid nil)
;; (org-agenda-format-date "") ;; Skip the date
;; (org-agenda-entry-types '(:deadline))
;; (org-deadline-warning-days 7)))
(todo
"PROG"
((org-agenda-overriding-header "Progress (Max=3)")))
(todo
"NEXT"
((org-agenda-overriding-header "Next (Max=5)")))
(todo
"ASK"
((org-agenda-overriding-header "Top Current Questions")))
(todo
"WAIT"
((org-agenda-overriding-header "Waiting"))))
((org-agenda-span 'day)
(org-agenda-files ,org-agenda-files))))
(defun my/org-agenda-planning-command (prefix name org-agenda-files)
"Helper function to generate planning view for the given agenda files."
`(,prefix
,name
((tags-todo "-{@.*}/TODO"
((org-agenda-overriding-header "Unplanned (options: NEXT, scheudle, @context)")))
;; Review completed tasks to keep my org files organized.
(todo "DONE|CANX"
((org-agenda-overriding-header "Completed (options: convert to note, delete, tag + archive)")
(org-agenda-todo-ignore-scheduled nil)))
;; (tags-todo "-{.*}"
;; ((org-agenda-overriding-header "Untagged Tasks")))
;; (tags "@jing|@casper|@cayden"
;; ((org-agenda-overriding-header "Family Tasks")))
;; (stuck)
)
((org-agenda-files ,org-agenda-files)
(org-stuck-projects
'("+expectation+LEVEL=2/-DONE-CANX-TODO-NEXT-PROG" ("NEXT" "PROG"))))))
(defun my/org-agenda-context-command (prefix name org-agenda-files)
"Helper function to generate planning view for the given agenda files."
`(,prefix
,name
((tags-todo
"{@.*}" ; todo with a tag started with "@"
((org-agenda-overriding-header "Context-based Tasks")))
(todo
"ASK"
((org-agenda-overriding-header "Ask"))))
((org-agenda-files ,org-agenda-files)
(org-agenda-todo-ignore-scheduled nil))))
;; https://orgmode.org/worg/org-tutorials/org-custom-agenda-commands.html
;; Notes:
;; - "tags" search is slow.
;; - Use "-{.*}" to match nothing.
(setq org-agenda-custom-commands
`(,(my/org-agenda-planning-command
"p" "Planning (Personal)"
`(list ,my/org-tasks-file))
,(my/org-agenda-planning-command
"P" "Planning (Work)"
`(list ,my/org-work-file))
,(my/org-agenda-context-command
"c" "Context (Personal)"
`(list ,my/org-tasks-file))
,(my/org-agenda-context-command
"C" "Context (Work)"
`(list ,my/org-work-file))
,(my/org-agenda-daily-command
"n" "Daily Agenda (Personal)"
`(list ,my/org-inbox-file
,my/org-tasks-file))
,(my/org-agenda-daily-command
"N" "Daily Agenda (Work)"
`(list ,my/org-inbox-file
,my/org-work-file))))
;; 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)
;; In todo view, hide all scheduled todo but not deadlined.
(setq org-agenda-todo-ignore-scheduled 'all)
(setq org-agenda-todo-ignore-deadlines nil)
;; In tag search view, hide all scheduled todo like tags-todo.
(setq org-agenda-tags-todo-honor-ignore-options t)
;; In agenda view, hide all done todo in agenda.
(setq org-agenda-skip-scheduled-if-done t)
(setq org-agenda-skip-deadline-if-done t)
;; Hide task until the scheduled date.
(setq org-agenda-skip-deadline-prewarning-if-scheduled 'pre-scheduled)
;; Make blocked tasks invisible in the agenda.
(setq org-agenda-dim-blocked-tasks t)
;; Save some spaces.
(setq org-agenda-use-time-grid nil)
;; Chagne: Add deadline-up to agenda to separate schedule and deadline.
(setq org-agenda-sorting-strategy
'((agenda deadline-up habit-down time-up priority-down category-keep)
(todo priority-down category-keep)
(tags priority-down category-keep)
(search category-keep)))
(define-advice org-agenda
(:around (orig-fun &rest args) with-org-directory)
(let ((default-directory org-directory))
(apply orig-fun args))))
Useful references:
Agenda Prefixes
Add extra information (repeater and effort) to the agenda view. See this blog post for the details.
;; Shorten the leaders to reserve spaces for the repeater.
(setq org-agenda-scheduled-leaders '("Sched" "S.%2dx"))
(setq org-agenda-deadline-leaders '("Deadl" "In%2dd" "D.%2dx"))
(defun my/org-agenda-repeater ()
"The repeater shown in org-agenda-prefix for agenda."
(let ((pom (org-get-at-bol 'org-marker)))
(if (or (org-get-scheduled-time pom) (org-get-deadline-time pom))
(format "%5s: " (or (org-get-repeat) ""))
"------------")))
;; Add repeater and effort to the agenda prefixes.
(setq org-agenda-prefix-format
'((agenda . " %i %-12:c%?-12t%s%(my/org-agenda-repeater)")
(todo . " %i %-12:c%5e ")
(tags . " %i %-12:c%5e ")
(search . " %i %-12:c%5e ")))
org-capture - Capturing
(use-package org-capture
:ensure org
:bind ("C-c c" . org-capture)
:config
;; 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")
(file "~/org/.template/inbox.org")))
(add-to-list
'org-capture-templates
`("I" "Inbox (Work)" entry (file+headline ,my/org-work-file "Inbox")
(file "~/org/.template/inbox.org")))
(add-to-list
'org-capture-templates
`("p" "Project" entry (file+headline ,my/org-tasks-file "Inbox")
(file "~/org/.template/project.org")))
(add-to-list
'org-capture-templates
`("P" "Project (Work)" entry (file+headline ,my/org-work-file "Inbox")
(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")))
org-attach - 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
.
(use-package org-attach
:ensure org
:after org :demand ; load with org
:config
(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:
org-habit
org-habit
with a small set (2) of habits.
(use-package org-habit
:disabled
:ensure org
:after org :demand ; load with org
:config
(setq org-habit-graph-column 80
org-habit-preceding-days 14
org-habit-following-days 0
org-habit-show-all-today nil
org-habit-show-habits-only-for-today nil))
Archive
I archive outdated org headings to the same file under the top level heading “Archive”.
;; Archive to the same file under "* Archive"
;; Alternative: Archive to the same heading with `org-archive-to-archive-sibling' (C-c C-x A).
(setq org-archive-location "::* Archive")
This makes searching the archived content easier, compared to archiving to a separate file,.
In the future, when my org files become too large and too slow, I might archive them to a separate file to keep the main org file small and improve agenda speed. I may then need to modify the Elisp code to enable searching both the org file and its archive file.
Refile
;; 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
org-outline-path-complete-in-steps nil)
(setq org-refile-targets `((nil :maxlevel . 2)))
;; Refile to the beginning of a file or entry.
(setq org-reverse-note-order t)
Jump to org-refile-last-stored
after Refiling
After refiling an org heading (org-refile
), I usually go to the new location and adjust the position with nearby headings. Adding an advice to automate this.
(define-advice org-refile
(:after (&rest args) my/bookmark-jump-org-refile-last-stored)
(bookmark-jump "org-refile-last-stored"))
Jump to a Random Org Heading
When organizing all notes in a giant org file, it is useful to review them with this function.
(with-eval-after-load "org"
(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))
(my/org-reveal))))
(keymap-set org-mode-map "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
- it is closes to other narrowing commands with prefix
C-x n
, and - it is easy to type as it repeats the last key
n
.
(with-eval-after-load "org"
(keymap-set org-mode-map "C-x n n" #'org-toggle-narrow-to-subtree))
Reveal entry and children of the current org heading
(defun my/org-reveal(&optional hide-other-headings)
"Show entry and children of the current org heading."
(interactive)
(when (derived-mode-p 'org-mode)
;; Note: 'org-cycle-*' and 'org-fold-*' below are not not
;; available in Emacs 28 or below.
(when (fboundp 'org-cycle-overview)
(when hide-other-headings (org-cycle-overview))
(org-fold-show-entry)
(org-fold-show-children))
;; Passing '(4) (C-u): on each level of the hierarchy all siblings
;; are shown.
(org-fold-reveal '(4))
(recenter)))
(defun my/org-reveal-keep-other(&rest args)
(my/org-reveal))
(defun my/org-reveal-hide-other(&rest args)
(my/org-reveal t))
(advice-add 'org-agenda-goto :after #'my/org-reveal-hide-other)
(advice-add 'org-agenda-switch-to :after #'my/org-reveal-hide-other)
(advice-add 'org-clock-goto :after #'my/org-reveal-hide-other)
(advice-add 'org-refile :after #'my/org-reveal-keep-other)
(advice-add 'consult-line :after #'my/org-reveal-keep-other)
(advice-add 'consult-grep :after #'my/org-reveal-keep-other)
(advice-add 'consult-ripgrep :after #'my/org-reveal-keep-other)
(advice-add 'consult-org-heading :after #'my/org-reveal-keep-other)
(advice-add 'consult-org-agenda :after #'my/org-reveal-keep-other)
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."))
org-babel - literate programming
https://orgmode.org/worg/org-contrib/babel/languages/index.html
(use-package ob
:ensure org
:autoload (org-babel-tangle-file)
:config
(org-babel-do-load-languages
'org-babel-load-languages
'((emacs-lisp . t)
;; (shell . t)
;; (python . t)
;; graph and diagram
;; (gnuplot . t) (ditaa . t) (dot . t)
))
(setq org-edit-src-content-indentation 0)
;; Silent result by default. Append to override the default value.
;;
;; To override it in org file level:
;; #+PROPERTY: header-args:emacs-lisp :results replace
;; Also see https://orgmode.org/manual/Using-Header-Arguments.html
(add-to-list 'org-babel-default-header-args '(:results . "output") t)
(add-to-list 'org-babel-default-header-args:emacs-lisp '(:results . "silent") t)
;; (add-to-list 'org-babel-default-header-args:python '(:results . "replace") t)
;; Use python3.
(setq org-babel-python-command "python3")
;; Skip confirming for some languages.
;; See https://orgmode.org/manual/Code-Evaluation-Security.html
(defun my/org-confirm-babel-evaluate (lang body)
(not (or (string= lang "gnuplot")
(string= lang "ditaa")
(string= lang "dot"))))
;; (setq org-confirm-babel-evaluate #'my/org-confirm-babel-evaluate)
(setq org-confirm-babel-evaluate nil))
There are drawback of using literate programming (Org-Babel), discussed in this reddit post. For example, no auto-complete, LSP, and IDE style feature.
org-babel - Enable Emacs lisp lexical binding by default
To enable lexical binding in org-babel
, set the :lexical
to t
in the header
- use
#+begin_src emacs-lisp :lexical t
for a singlebegin_src
block, or - add
'(:lexical . "yes")
to the default header args for all src blocks, like what I do below.
(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.
(lambda ())
If Enabled | If Disabled | |
---|---|---|
Emacs 29 | closure (t) nil | (lambda nil) |
Emacs 30 | #[nil (nil) t] | #[nil (nil) nil] |
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.
Blank lines before new entry
I prefer keeping a blank line before and after the heading. The elisp below ensure the “before” case.
(setq org-blank-before-new-entry '((heading . t) (plain-list-item . nil)))
Respect content when inserting heading
(setq org-insert-heading-respect-content t)
Also see https://www.n16f.net/blog/org-mode-headline-tips/.
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:
- I add
-*- eval: (org-auto-tangle-mode); -*-
to the org file that wants auto-tangle. - I set
org-auto-tangle-default
tot
to enable auto-tangle in all Org buffers with the minor mode activated.
(use-package org-auto-tangle
:config (setq org-auto-tangle-default t))
A supplemental functions to tangle my dot files managed by org babel. This is needed to update the files on multiple computers.
(defun my/tangle-dot-files ()
(interactive)
(org-babel-tangle-file "~/.emacs.d/bash.org")
(org-babel-tangle-file "~/.emacs.d/tmux.org"))
emacs
, bash
and tmux
are three critical pieces of my computing experiences on any machine. Hence, I put them all in .emacs.d
. With a git clone
or git pull --rebase
, I can get all these up-to-dated.
Disallow M-RET to split lines
(with-eval-after-load "org"
(setq org-M-RET-may-split-line '((default . nil))))
Clocking
(use-package org-clock
:ensure org
;; Allows going to the clock entry from everywhere.
:bind ("C-c C-x C-j" . org-clock-goto)
:init
(setq org-log-done 'time)
(setq org-log-into-drawer t)
;; 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))
org-indent
(use-package org-indent
:diminish org-indent-mode
:ensure org
;; Enable the `org-indent-mode' by default.
:init (setq org-startup-indented t))
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.
;; Set #+STARTUP: fold by default.
(setq org-startup-folded t)
Images
(with-eval-after-load "org"
;; Whether to display the inline image at startup.
(setq org-startup-with-inline-images nil)
;; Redisply the inline image after org babel execute.
;; https://emacs.stackexchange.com/a/30539
(add-hook 'org-babel-after-execute-hook 'org-redisplay-inline-images)
;; 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 toggle the preview, use
M-x org-latex-preview
orC-c C-x C-l
. - To enable preview on startup for an org file, add
#+STARTUP: latexpreview
to the header. - To enable preview on startup globally, add
(setq org-startup-with-latex-preview t)
toinit.el
. - To customize the preview, set
org-format-latex-options
.
(with-eval-after-load "org"
;; 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. π, Γ, ∑.
- Set
org-pretty-entities
tot
to enable this feature globally - Use
M-x org-toggle-pretty-entities
to toggle this feature on demand.
;; 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.
Also see https://github.com/misohena/el-easydraw, a fancy drawing package.
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),
- I run
org-element-cache-reset
, or - I disable the cache. However, it can slow down heavy org operations, like org agenda, by quite a lot.
;; (setq org-element-use-cache nil)
ox-hugo - Blogging
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.
- In ox-hugo, uses
#+begin_video
to generate the<video>
HTML5 tag (details in ox-hugo/issues/274). - In Hugo config, set
markup.goldmark.renderer.unsafe
totrue
(details in discourse.gohugo.io).
(use-package ox-hugo
:after ox :demand
:config
;; For per-file configuration, use:
;; #+HUGO_BASE_DIR: ~/org/blog/
(setq org-hugo-base-dir (expand-file-name "blog" org-directory))
;; By default, export to the /content/.
(setq org-hugo-section "")
(add-to-list 'org-hugo-external-file-extensions-allowed-for-copying "webm"))
Related blog posts:
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)
Allow Alphabetical List
This allows alphabetical list like:
- Monday
- Tuesday
- Wednesday
(setq org-list-allow-alphabetical t)
org-drill - Spaced Repetition
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)
;; Show Google Translate result with the answers.
(defun my/google-translate-org-heading()
(interactive)
(google-translate-translate
google-translate-default-source-language
google-translate-default-target-language
(org-entry-get nil "ITEM")))
(add-hook 'org-drill-display-answer-hook #'my/google-translate-org-heading)
(define-advice org-drill
(:before (&rest args) delete-other-windows)
"Delete other windows during the org-drill session"
(delete-other-windows))
;; fix the terminal issue: https://gitlab.com/phillord/org-drill/-/issues/44
(define-advice org-drill-present-default-answer
(:override (session reschedule-fn) fix-terminal)
(prog1 (cond
((oref session drill-answer)
(org-drill-with-replaced-entry-text
(format "\nAnswer:\n\n %s\n" (oref session drill-answer))
(funcall reschedule-fn session)
))
(t
(org-drill-hide-subheadings-if 'org-drill-entry-p)
(org-drill-unhide-clozed-text)
(org-drill--show-latex-fragments)
(ignore-errors
(org-display-inline-images t))
(org-cycle-hide-drawers 'all)
(org-remove-latex-fragment-image-overlays)
(save-excursion
(org-mark-subtree)
(let ((beg (region-beginning))
(end (region-end)))
(when (window-system) ; check if it is a (window-system)
(org--latex-preview-region beg end)))
(deactivate-mark))
(org-drill-with-hidden-cloze-hints
(funcall reschedule-fn session)))))))
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.
Better Org-mode in Terminal
C-c C-,
is the keybinding for org-insert-structure-template
but C-,
does not work in a terminal.
(with-eval-after-load "org"
(keymap-set org-mode-map "C-c ," #'org-insert-structure-template))
org-cliplink
(use-package org-cliplink)
Reading
Google Translate
(use-package google-translate
:autoload (google-translate-translate)
:bind
("C-c v" . google-translate-at-point)
("C-c V" . my/google-translate-at-point-in-popup)
:init
(setq google-translate-default-source-language "auto") ;; or "en"
(setq google-translate-default-target-language "zh-TW")
(setq google-translate-output-destination 'help)
:config
(defun my/google-translate-at-point-in-popup ()
"Like `google-translate-at-point' with the
`google-translate-output-destination' as `popup'."
(interactive)
(let ((google-translate-output-destination 'popup))
(google-translate-at-point))))
Ebook with nov
Reading ebooks within Emacs allows me to reuse the Emacs workflow, such as capturing with Org mode, translating, asking questions with LLMs, and so on.
(use-package nov
:mode ("\\.epub\\'" . nov-mode)
:config
(setq nov-unzip-program (executable-find "bsdtar")
nov-unzip-args '("-xC" directory "-f" filename))
;; nov-mode-map map <home> and <end> to beginning and end of the buffer.
;; Unset it so it is using the default beginning and end of the line.
(keymap-unset nov-mode-map "<home>")
(keymap-unset nov-mode-map "<end>"))
Writing
Spell Checking with flyspell-correct-wrapper
(use-package flyspell
:diminish (flyspell-mode flyspell-prog-mode)
:hook
(text-mode-hook . flyspell-mode)
(prog-mode-hook . flyspell-prog-mode)
:config
;; Removes the overlay properties which flyspell uses on incorrect words for mouse operations.
;; https://emacs.stackexchange.com/a/55708
(define-advice make-flyspell-overlay
(:filter-return (overlay) make-flyspell-overlay-return-mouse-stuff)
(overlay-put overlay 'help-echo nil)
(overlay-put overlay 'keymap nil)
(overlay-put overlay 'mouse-face nil)))
(use-package flyspell-correct
:after flyspell
:bind (:map flyspell-mode-map ("M-;" . flyspell-correct-wrapper)))
Use a Personal Dictionary
Chris has a blog post on why and how to use a personal dictionary. I store my personal dictionary in the Emacs directory and version control it along with my Emacs configuration. I use flyspell-correct-wrapper
correct and add vocab to the dictionary.
(setq ispell-personal-dictionary
(expand-file-name ".personal-dictionary.pws" user-emacs-directory))
Auth Source
I use auth-source
to manage secrets, mainly for api keys.
(use-package auth-source
:config
(add-to-list 'auth-sources (expand-file-name ".authinfo" user-emacs-directory))
(add-to-list 'auth-sources (expand-file-name ".authinfo.nogit" user-emacs-directory)))
Note that auth-sources also support pass
, the unix password manager, with auth-source-pass-get
.
LLM
gptel
(use-package gptel
:bind
("C-c l l" . gptel)
("C-c l w" . gptel-rewrite)
("C-c l s" . my/gptel-model-switch)
("C-c l m" . gptel-menu)
:config
(setq gptel-model 'gemini-2.5-flash-preview-04-17)
(setq gptel-backend (gptel-make-gemini "Gemini"
:key (auth-source-pick-first-password :host "gemini")
:stream t))
(defun my/gptel-model-switch ()
"Switch the LLM provider."
(interactive)
(let ((selected (consult--read
gptel--gemini-models
:prompt "Switch gptel-model: "
:require-match t
:history 'my/gptel-model-switch)))
(message "Switched gptel-model to '%s'" (intern selected))
(setq gptel-model (intern selected))))
(setq gptel-default-mode 'org-mode)
(setq gptel-prompt-prefix-alist '((org-mode . "* ")))
(setq gptel-log-level 'info)
(defun my/gptel-custom-faces (&rest _)
(modus-themes-with-colors
(custom-set-faces
;; Make gptel highlight more visible.
`(gptel-rewrite-highlight-face ((,c :background ,bg-green-nuanced))))))
(my/gptel-custom-faces)
(add-hook 'modus-themes-after-load-theme-hook #'my/gptel-custom-faces))
Version Control
magit
There are multiple key-binding to trigger magit:
C-x g
- magit-status
C-c g
- magit-dispatch
C-c 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
:config
(setq magit-define-global-key-bindings 'recommended)
;; always visit file in another window.
(define-advice magit-diff-visit-file
(:override (file &optional other-window) visit-file-other-window)
(magit-diff-visit-file-other-window file)
(my/org-reveal)))
;; this binds `magit-project-status' to `project-prefix-map' when
;; project.el is loaded.
(use-package magit-extras
:ensure magit
:after project :demand)
diff-mode
(use-package diff-mode
;; Unset M-o (`diff-goto-source') which is my binding for `other-window'
:config (keymap-unset diff-mode-map "M-o"))
Diff Highlight with diff-hl
(use-package diff-hl
:hook (find-file-hook . diff-hl-mode)
:config
;; Added in https://github.com/dgutov/diff-hl/pull/207
(setq diff-hl-update-async t)
(diff-hl-flydiff-mode +1)
;; Automatic diff-hl-margin-mode in terminal.
;; See https://github.com/dgutov/diff-hl/issues/155.
(add-hook 'diff-hl-mode-on-hook
(lambda ()
(unless (display-graphic-p)
(diff-hl-margin-local-mode))))
(add-hook 'magit-pre-refresh-hook #'diff-hl-magit-pre-refresh)
(add-hook 'magit-post-refresh-hook #'diff-hl-magit-post-refresh))
Project Management
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-package project
:autoload (project--read-file-absolute)
;; Use the entire "project-prefix-map" in "project-switch-project".
:config (setq project-switch-use-entire-map nil))
Find File 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.
(setq my/known-project-roots '("~/org/" "~/org/blog/" "~/.emacs.d/" "~/repo/dotfiles/"))
(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)))))
my/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))))
(keymap-set project-prefix-map "a" #'my/project-find-file-in-known-projects)
Full Text Search 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)
""))
(keymap-set project-prefix-map "g" #'my/consult-ripgrep-current-project)
Programming
Compilation
(setq compilation-scroll-output t)
Corfu
(use-package corfu
;; Enable Corfu only for certain modes. See also `global-corfu-modes'.
:hook ((prog-mode-hook eshell-mode-hook comint-mode-hook) . corfu-mode)
:config
(setq corfu-auto t)
(setq corfu-quit-no-match t)
;; enable orderless style completion
(setq corfu-quit-at-boundary nil))
(use-package corfu-terminal
:unless (display-graphic-p)
:after corfu
:init (corfu-terminal-mode +1))
Eglot
(use-package eglot
:bind
(:map eglot-mode-map
("M-l r" . eglot-rename)
("M-l TAB" . eglot-format)
("M-RET" . eglot-code-actions)))
Xref
By default, TAB
maps to xref-quit-and-goto-xref
. I don’t like losing the xref buffer if hitting TAB
accidentally.
(use-package xref
:config (keymap-set xref--xref-buffer-mode-map "TAB" #'xref-goto-xref))
Python
Python runs in Emacs out of the box: M-x run-python
.
(use-package pyvenv)
Java / Android
;; For gradle config files
(use-package groovy-mode)
Kotlin Mode
(use-package kotlin-mode)
Swift Mode
(use-package swift-mode)
Web Mode
https://web-mode.org/ is an autonomous major-mode for editing web templates.
(use-package web-mode
:mode ("\\.html\\'" . web-mode)
:config
;; TODO: Use .dir.local because not all .html are go template
(setq web-mode-engines-alist
'(("go" . "\\.html\\'"))))
JavaScript Mode
(setq js-indent-level 2)
YAML Mode
(use-package yaml-mode)
Markdown Mode
(use-package markdown-mode
:bind
(:map markdown-mode-map
("M-<left>" . markdown-promote)
("M-<right>" . markdown-demote)
("M-S-<left>" . markdown-promote-subtree)
("M-S-<right>" . markdown-demote-subtree)))
Caddyfile Mode
(use-package caddyfile-mode
:mode (("Caddyfile\\'" . caddyfile-mode))
:config
(defun my/caddyfile-mode-style ()
(setq-local tab-width 2))
(add-hook 'caddyfile-mode-hook #'my/caddyfile-mode-style))
Systemd Mode
(use-package systemd)
Bazel Mode
For syntax highlighting in org source block bazel-starlark
.
(use-package bazel)
SSH Config Mode
(use-package ssh-config-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)
Competitive Programming
Auto Insert the C++ Template
(use-package autoinsert
:config
(setq auto-insert-directory (expand-file-name "auto-insert/" user-emacs-directory))
(define-auto-insert "competitive/.*\.cpp" "template.cpp")
;; Uncomment to enable auto-insert-mode globally. Otherwise, use "M-x
;; auto-insert" on demand.
(auto-insert-mode))
;; Uncomment to enable eglot for c++ mode.
;; (add-hook 'c++-mode-hook #'eglot-ensure)
Compile and Run
See https://codeforces.com/blog/entry/101292.
(defun my/compile-and-run()
(interactive)
(if (derived-mode-p 'c++-mode)
(let* ((src (file-name-nondirectory (buffer-file-name)))
(exe (concat "/tmp/" (file-name-sans-extension src))))
(compile
(string-join
`("g++" "-std=c++17" "-O2" "-Wall" "-Wno-sign-compare" ,src "-o" ,exe
"&&" "time" ,exe "<" "/tmp/input.txt")
" ")))
(recompile)))
(keymap-global-set "<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)
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
(if (string-prefix-p "/sudo:" file-name)
file-name
(concat "/sudo::" (expand-file-name file-name)))))
(find-file tramp-file-name)))
;; Allow root-owned auto-save, backup or lock files in "/tmp".
(setq tramp-allow-unsafe-temporary-files t)
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 (setq default-input-method "rime")
:config
(when (eq system-type 'darwin)
;; 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")
;; 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)))
(when (eq system-type 'berkeley-unix)
;; https://github.com/DogLooksGood/emacs-rime/issues/224
;; 1. fix gmake
;; 2. set "CC = /usr/bin/cc" in Makefile
(setq rime-emacs-module-header-root "/usr/local/include/")
;; pkg install zh-librime
(setq rime-librime-root "/usr/local"))
;; 'posframe provides the best UI (candidates right below the
;; cursow) but could be slow in large org file. 'sidewindow is fast
;; in all kinds of buffers but the the candidates are too far from
;; the cursor.
(setq rime-show-candidate 'posframe
rime-posframe-style 'vertical)
; (setq rime-show-candidate 'sidewindow
; rime-sidewindow-side 'bottom
; rime-sidewindow-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))
Set recipient for encrypting files
https://www.gnu.org/software/emacs/manual/html_node/epa/Encrypting_002fdecrypting-gpg-files.html
- Name the file as
filename.ext.gpg
- Set
epa-file-encrypt-to
in file header.
-*- epa-file-encrypt-to: ("whhone@gmail.com") -*-
;; The code below does not work. It tries to encrypt all files!
;; (setq-default epa-file-encrypt-to '("whhone@gmail.com"))
Pass
(use-package pass)
Using Emacs in a Terminal
This section make Emacs usable in a terminal.
Copy to Native Clipboard with OSC 52
Clipetty is a minor mode that sends text killed in terminal Emacs to the Operating System’s clipboard. It requires the terminal to support OSC 52.
I don’t use clipetty-mode
nor global-clipetty-mode
because they send way too many texts to the system clipboard. I only send text in two cases: pressing M-w
and open a link.
(use-package clipetty
:hook ((pass-mode-hook magit-status-mode-hook) . clipetty-mode)
:autoload (clipetty--emit)
:init
;; Ensure the shell command is executed without Tramp, which gives
;; wrong SSH_TTY. https://github.com/spudlyo/clipetty/issues/35
(define-advice clipetty--get-tmux-ssh-tty
(:around (orig-fun &rest args) with-local-directory)
(let ((default-directory user-emacs-directory))
(apply orig-fun args)))
(defun my/with-clipetty-advice (orig-fun &rest args)
"Execute ORIG-FUN with clipetty mode enabled, if region is active."
(if (and (boundp 'clipetty-mode)
clipetty-mode)
(apply orig-fun args)
;; only enable clipetty-mode when there is a selected region.
(when (use-region-p) (clipetty-mode))
(apply orig-fun args)
(clipetty-mode 0)))
;; Enable clipetty for kill-ring-save (M-w) & kill-region (C-w).
;;
;; Not using clippetty-kill-ring-save because it does not work with
;; whole-line-or-region-mode.
(advice-add 'kill-ring-save :around #'my/with-clipetty-advice)
;; Note (2025-03-09 Sun): trying to use only kill-ring-save.
(advice-add 'kill-region :around #'my/with-clipetty-advice)
(define-advice browse-url
(:around (orig-fun &rest args) copy-url-if-termainl)
(if (display-graphic-p)
(apply orig-fun args)
(let ((url (nth 0 args)))
(message "Clipetty link: %s" url)
(clipetty--emit (clipetty--osc url t))))))
Open Link with Consistent Key Binding C-c C-o
In Emacs, there are two key ways to open a link:
C-c C-o
fororg-mode
.C-c RET
for other modes withgoto-address-mode
enabled.
Having two key bindings to open link is confusing. Instead, I make the org-mode
binding work everywhere.
When inside a terminal, it copies link into clipboard with OSC 52 so that I can paste it to local browser.
(defun my/find-link-at-point()
"Returns the link at point. Improved version of the `browse-url-at-point'."
(or (thing-at-point 'url t)
(get-text-property (point) 'shr-url)
(and (derived-mode-p 'org-mode)
(org-element-property :raw-link (org-element-context)))))
(defun my/browse-url-at-point ()
"Browse the URL at point with special cases handling."
(interactive)
(when-let (url (my/find-link-at-point))
(browse-url url)
t))
(keymap-global-set "C-c C-o" #'my/browse-url-at-point)
;; Have `org-open-at-point' to try `my/browse-url-at-point' before
;; built-in function.
(with-eval-after-load "org"
(add-hook 'org-open-at-point-functions #'my/browse-url-at-point))
This is inspired by Brain dump – Controlling link opening in Emacs.
xterm-paste advice region
(define-advice xterm-paste
(:before (&args) delete-active-region)
(when (use-region-p)
(delete-active-region)))
Enable Mouse
(use-package xt-mouse
:init (xterm-mouse-mode +1))
Changing the window vertical border in terminal
(set-display-table-slot standard-display-table 'vertical-border ?│)
term
(use-package term
:config
;; Use line mode by default.
(define-advice ansi-term
(:after (&rest args) line-mode)
(term-line-mode))
(define-advice term
(:after (&rest args) line-mode)
(term-line-mode))
(define-advice term-handle-exit
(:after (&rest args) kill-buffer)
(kill-buffer (current-buffer))))
eshell
(use-package eshell
:bind ("C-c t" . eshell)
:config
;; Prompt function
(setq eshell-prompt-function
(lambda ()
(modus-themes-with-colors
(concat
"\n"
(propertize (user-login-name) 'face 'eshell-ls-missing)
(propertize (format " @%s" (system-name)) 'face '(:weight bold :inherit 'eshell-prompt))
(propertize (format " %s" (abbreviate-file-name (eshell/pwd))) 'face 'eshell-ls-directory)
(unless (eshell-exit-success-p) (format " [%d]" eshell-last-command-status))
"\n> ")))))
Proced
(use-package proced
:config
(setq proced-enable-color-flag t)
(setq-default proced-auto-update-flag 'visible))
Mac OS
;; These are some key bindings ported from MacOS to everywhere, since
;; I found they are handy.
;; MacOS uses Command+s for "save".
(keymap-global-set "M-s" #'save-buffer)
(with-eval-after-load "org-agenda"
(keymap-set org-agenda-mode-map "M-s" #'org-save-all-org-buffers))
(when (eq system-type 'darwin)
;; MacOS put the menu-bar in the top bar which already using the space.
(when (display-graphic-p) (menu-bar-mode 1))
;; If swapped "control" and "command" key on MacOS.
;; (setq mac-command-modifier 'control)
;; (setq mac-control-modifier 'meta)
;; (setq mac-option-modifier 'super)
;; If not,
(setq mac-command-modifier 'meta)
(keymap-global-set "<home>" #'move-beginning-of-line)
(keymap-global-set "<end>" #'move-end-of-line)
;; MacOS uses Command+Control+f to toggle fullscreen. Also make it
;; work with Emacs.
(keymap-global-set "C-M-f" #'toggle-frame-fullscreen))
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")))
(my/add-to-exec-path (expand-file-name ".local/bin/aider" (getenv "HOME")))
(when (eq system-type 'darwin)
(my/add-to-exec-path "/opt/homebrew/bin/"))
HTML Rendering
(setq shr-max-width 80)
(setq shr-use-colors nil)
Ask Before Closing a Frame
When using emacsclient
, confirm before closing the client frame.
(define-advice server-save-buffers-kill-terminal
(:before (&rest _args) warn-before-killing)
"Ask whether you really want to `server-save-buffers-kill-terminal'."
(unless (y-or-n-p "Close the client frame?")
(user-error "Aborted")))
Copy filename
(defun my/show-and-copy-filename ()
"Show and copy the path to the current file in the minibuffer."
(interactive)
(let ((file-name (buffer-file-name)))
(unless file-name (user-error "Buffer not visiting a file"))
(kill-new file-name)
(clipetty--emit (clipetty--osc file-name t))
(minibuffer-message "%s" file-name)))
(keymap-global-set "C-c n" #'my/show-and-copy-filename)
Load Private Config
(let ((private-config-el (expand-file-name "private.el" user-emacs-directory)))
(when (file-exists-p private-config-el)
(load-file private-config-el)))
Major Changes
- Use use-package lazy loading to reduce startup time from > 5s to < 0.5s.
- Add config to use
whole-line-or-region
for more modern editing. - Add config to make repeated tasks look different in org agenda.
- Add config to improve window splitting.
- Add config to have kill (copy/cut) under terminal works with OS’s clipboard.
- Add config to open link with consistent key binding.
- Add config to manage
ispell-personal-dictionary
with version control. - Add
my/jump-org-heading-advice
to show entry and children afterconsult-org-heading
. - Add config that jumps to the refiled location after
org-refile
.