Emacs Config
This is my public Emacs configuration.
General
Startup
;; Increase the garbage collection threshold to 100MB to reduced startup time.
;; See https://www.reddit.com/r/emacs/comments/3kqt6e
(setq gc-cons-threshold (* 1024 1024 100))
;; Turn off mouse interface early in startup to avoid momentary display
(menu-bar-mode -1)
(tooltip-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)
;; Inhibit the startup screen and shorten the scratch buffer's message.
(setq inhibit-startup-screen t)
(setq initial-scratch-message ";; scratch buffer\n\n")
Hydra
Make sure Hydra is ready before the defhydra
below. Also see https://github.com/abo-abo/hydra/wiki/Hydras-by-Topic.
(use-package hydra
:commands defhydra)
Quitting
A popular recommendation is to bind <escape>
with keyboard-escape-quit
. However, it closes all the other windows at the same time and it is annoying to me. Instead, I map <escape>
to keyboard-quit
.
;; Don't forget to handle minibuffer with "minibuffer-keyboard-quit".
(global-set-key (kbd "<escape>") 'keyboard-quit)
;; Avoid entering the ~repeat-complex-cammand~ when quiting everything with ~C-x~.
(global-unset-key (kbd "C-x <escape> <escape>")) ; repeat-complex-command
An even better way is to build muscle memory to use the standard C-g
to “quit” and C-]
to “abort”.
Backups
I don’t use backup files often as I use git to manage most of my org files. However, I still feel safer when having a backup. I follow the backup configuration from Sacha Chua to enable Emacs’s backups aggressively.
;; Save backup files to a dedicated directory.
(setq backup-directory-alist '(("." . "~/.emacs.d/backups")))
(setq delete-old-versions -1)
;; Make numeric backup versions unconditionally.
(setq version-control t)
(setq vc-make-backup-files t)
(setq auto-save-file-name-transforms '((".*" "~/.emacs.d/auto-save-list/" t)))
;; Do not create lock files.
(setq create-lockfiles nil)
Disable Native Compilation Warnings
(setq native-comp-async-report-warnings-errors nil)
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)
;; 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)
Editing
Remove Key Bindings
I disable most Emacs’s default keybinding because I am not able to get my muscle memory to work with it. Even if I do, that muscle memory probably will cause trouble when editing in other applications.
;; Disabe some Emacs's default keybindings.
(global-unset-key (kbd "C-v")) ; scroll-up-command
(global-unset-key (kbd "M-v")) ; scroll-down-command
(global-unset-key (kbd "C-n")) ; next line
(global-unset-key (kbd "C-p")) ; previous line
(global-unset-key (kbd "C-b")) ; previous char
(global-unset-key (kbd "C-f")) ; next char
(global-unset-key (kbd "C-t")) ; switch char
(global-unset-key (kbd "C-l")) ; recenter
(global-unset-key (kbd "C-j")) ; (electric-newline-and-maybe-indent)
(global-unset-key (kbd "M-l")) ; downcase-word
(global-unset-key (kbd "M-u")) ; upcase-wrod
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 ()
(visual-line-mode +1) ; enable "word-wrap"
(toggle-truncate-lines -1)
;; (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)
(goto-address-mode +1)
(flyspell-mode))
(defun my/prog-mode ()
(display-line-numbers-mode +1)
(my/edit-mode)
(show-paren-mode +1)
(goto-address-prog-mode +1)
(flyspell-prog-mode))
(add-hook 'text-mode-hook 'my/text-mode)
(add-hook 'prog-mode-hook 'my/prog-mode)
(add-hook 'conf-mode-hook 'my/prog-mode)
;; 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 my/save-buffer
.
;; 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 (`before-save-hook') 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))))
(add-hook 'before-save-hook #'my/delete-trailing-whitespace-except-current-line)
Ignore save-buffer
unless visiting a file
See my blog post.
(defun my/save-buffer (&optional arg)
"Like `save-buffer', but does nothing if buffer is not visiting a file."
(interactive "p")
(unless (or (buffer-file-name) ; regular buffer
(buffer-file-name (buffer-base-buffer))) ; indirect buffer
(user-error "Use 'M-x save-buffer' to save this buffer."))
(progn
(my/delete-trailing-whitespace-except-current-line)
(save-buffer arg)))
(global-set-key [remap save-buffer] #'my/save-buffer)
Auto Save and Auto Revert
Auto-save and auto-revert makes Emacs more modern. They are especially useful when editing files that is synchronized with other computers. For example, it causes less conflict when editing the same Dropbox file on multiple places.
;; Auto save buffer if idled for 2 seconds.
(setq auto-save-timeout 2)
(auto-save-visited-mode +1)
;; Watch and reload the file changed on the disk.
(global-auto-revert-mode +1)
(setq auto-revert-remote-files t)
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.
;; 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)
- try drag-stuff.el
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.
(defun my/rename-file-and-buffer ()
"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") 'my/rename-file-and-buffer)
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)
(let ((filename (buffer-file-name)))
(if filename
(if (y-or-n-p (concat "Do you really want to delete file " filename " ?"))
(progn
(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)))
Insert Snippets
Very often, I want to insert today’s date. There are many ways of doing this.
(defun my/insert-current-date () (interactive)
(insert (shell-command-to-string "echo -n $(date +%Y-%m-%d)")))
(defhydra my/hydra-snippets (:hint nil
:foreign-keys warn
:exit t)
"Snippets"
("d" (my/insert-current-date) "Current Date (yyyy-mm-dd)")
("q" nil "Quit Menu"))
(global-set-key (kbd "C-c s") 'my/hydra-snippets/body)
Tramp
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.
;; for Emacs 28 or above
(setq tramp-verbose 2)
;; for Emacs 27 or below
(setq tramp-message-show-message nil)
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)
Appearance
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-org-blocks 'gray-background)
(setq modus-themes-headings
'((agenda-date . (1.2))))
(setq modus-themes-common-palette-overrides
'((fg-heading-1 blue-cooler)
(fg-heading-2 blue)
(fg-heading-3 blue-warmer)))
(load-theme 'modus-vivendi-tinted :no-confirm)
:bind ("<f5>" . modus-themes-toggle))
Doom Modeline
I don’t use Doom Emacs but I do use its modeline.
;; https://github.com/seagle0128/doom-modeline
(use-package doom-modeline
:init
(doom-modeline-mode +1))
Window Management
Winner Mode
Winner Mode is a global minor mode. When activated, it allows you to “undo” (and “redo”) changes in the window configuration with the key commands C-c left
and C-c right
.
(use-package winner
:init
(winner-mode +1))
Window Navigation
(use-package windmove
:bind
("M-s-<up>" . 'windmove-up)
("M-s-<down>" . 'windmove-down)
("M-s-<left>" . 'windmove-left)
("M-s-<right>" . 'windmove-right))
(use-package ace-window
:bind
("M-o" . 'ace-window)
:custom
(aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l))
(aw-scope 'frame))
Window Dedication
(defun my/toggle-current-window-dedication ()
(interactive)
(let* ((window (selected-window))
(dedicated (window-dedicated-p window)))
(set-window-dedicated-p window (not dedicated))
(message "Window %sdedicated to %s"
(if dedicated "no longer " "")
(buffer-name))))
(global-set-key (kbd "<f11>") 'my/toggle-current-window-dedication)
Focus to new Window after Split
I found it is more nature to focus to the new window after splitting.
(global-set-key (kbd "C-x 2")
(lambda ()
(interactive)
(split-window-vertically)
(other-window 1)))
(global-set-key (kbd "C-x 3")
(lambda ()
(interactive)
(split-window-horizontally)
(other-window 1)))
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-s" . consult-line)
("C-b" . consult-bookmark)
("C-j" . consult-imenu)
:config
(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-;" . embark-dwim) ;; good alternative: M-.
("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))
Hydra Consult Menu
Create a hydra menu for the consult commands I use frequently.
;; Hydra menu to for consult.
(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)
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"))
Reading
Dictionary
;; https://github.com/xuchunyang/youdao-dictionary.el
(use-package youdao-dictionary
:config
(setq url-automatic-caching t) ; enable cache
:bind
("C-c v" . youdao-dictionary-search-at-point+)
("C-c V" . youdao-dictionary-play-voice-at-point))
(defun my/browse-dictionary-at-point ()
(interactive)
(browse-url (concat "https://dictionary.cambridge.org/zht/詞典/英語-漢語-繁體/" (thing-at-point 'word))))
Google Translate
(use-package google-translate
:config
(setq google-translate-output-destination 'popup)
(setq google-translate-default-source-language "en")
(setq google-translate-default-target-language "zh-TW")
:bind
("C-c b" . google-translate-at-point))
(use-package pdf-tools
:config
(pdf-tools-install))
(use-package pdf-view-restore
:after pdf-tools
:config
(add-hook 'pdf-view-mode-hook 'pdf-view-restore-mode))
Writing
Spell Checking
(use-package flyspell-correct
:after flyspell
:bind (:map flyspell-mode-map ("C-<tab>" . flyspell-correct-wrapper)))
Synonyms
;; https://www.powerthesaurus.org/
(use-package powerthesaurus
:bind
("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")))
Personal Knowledge Management
Note Taking with denote
(defun my/denote-random-note (&optional directory)
"Open a random denote."
(interactive)
(let* ((denote-directory (or directory denote-directory))
(files (denote-directory-files)))
(find-file (nth (random (length files)) files))))
(defun my/denote-random-personal-note ()
"Open a random personal denote."
(interactive)
(my/denote-random-note (expand-file-name "personal" (denote-directory))))
(defun my/denote-random-work-note ()
"Open a random work denote."
(interactive)
(my/denote-random-note (expand-file-name "work" (denote-directory))))
(use-package denote
:bind
("C-c n n" . 'denote)
("C-c n f" . 'denote-open-or-create)
("C-c n k" . 'denote-keywords-add) ;; update file name automatically
("C-c n K" . 'denote-keywords-remove) ;; update file name automatically
("C-c n u" . 'denote-rename-file-using-front-matter)
("C-c n l" . 'denote-link-find-backlink)
("C-c n r" . 'my/denote-random-personal-note)
("C-c n R" . 'my/denote-random-work-note)
:init
(setq denote-directory (expand-file-name "~/sync/org/"))
:config
(setq denote-known-keywords '("emacs"))
(setq denote-prompts '(subdirectory title))
(setq denote-excluded-directories-regexp "attachments")
;; Makes the denote links different from usual link.
(set-face-attribute 'denote-faces-link
nil :foreground "magenta" :inherit 'link)
;; Remove the date and the identifier. They are duplicated with the file name.
;; I want to remove filetags too but denote-keyword-* need that.
(setq denote-org-front-matter "#+title: %1$s\n#+filetags: %3$s\n")
(add-hook 'dired-mode-hook #'denote-dired-mode))
;; allow empty keyword
(defun denote-rename-file-using-front-matter (file &optional auto-confirm)
(interactive (list (buffer-file-name) current-prefix-arg))
(when (buffer-modified-p)
(if (or auto-confirm
(y-or-n-p "Would you like to save the buffer?"))
(save-buffer)
(user-error "Save buffer before proceeding")))
(unless (denote-file-is-writable-and-supported-p file)
(user-error "The file is not writable or does not have a supported file extension"))
(if-let* ((file-type (denote-filetype-heuristics file))
(title (denote-retrieve-title-value file file-type))
(extension (file-name-extension file t))
(id (denote-retrieve-or-create-file-identifier file))
(dir (file-name-directory file))
(new-name (denote-format-file-name
dir id (denote-retrieve-keywords-value file file-type) (denote-sluggify title) extension)))
(when (or auto-confirm
(denote-rename-file-prompt file new-name))
(denote-rename-file-and-buffer file new-name)
(denote-update-dired-buffers))
(user-error "No front matter for title and/or keywords")))
(use-package consult-denote
:straight (consult-denote :type git :host codeberg :repo "whhone/consult-denote")
:after denote
:bind
("C-c n i" . 'consult-denote-link-insert-link)
("C-c n f" . 'consult-denote-open-or-create)
("C-c n F" . 'consult-denote-ripgrep))
Blogging with ox-hugo
My blog is generated by Hugo and ox-hugo.
(use-package ox-hugo
:after ox)
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
:custom
(org-drill-save-buffers-after-drill-sessions-p nil "Save buffers after drill sessions without prompt.")
(org-drill-maximum-items-per-session 10 "Reduce from the default 30 to make it to become a habit.")
: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.
Org-mode
Org Basic
(use-package emacs
:bind
(:map org-mode-map ("C-x n n" . org-toggle-narrow-to-subtree))
:custom
(org-directory "~/sync/org")
(org-indirect-buffer-display 'current-window)
(org-refile-use-outline-path 'file)
(org-startup-indented t)
(org-outline-path-complete-in-steps nil)
:config
;; Sets default-directory to org-directory so that =M-x magit= from the agenda view does not ask me for a dir.
(global-set-key (kbd "C-c a")
(lambda ()
(interactive)
(let ((default-directory org-directory)) (org-agenda))))
(global-set-key (kbd "C-c c") 'org-capture)
:hook
;; Reserve "C-c <arrow>" for windmove
(org-mode . (lambda ()
(local-unset-key (kbd "C-c <left>"))
(local-unset-key (kbd "C-c <right>"))
(local-unset-key (kbd "C-c <up>"))
(local-unset-key (kbd "C-c <down>")))))
Capturing
(use-package org-cliplink)
Attachment
I was using org-download
but switched to the built-in org-attach
in 2022. I use .dir-locals.el
to customize org-attach-id-dir
if necessary.
(require 'org-attach)
(setq org-attach-id-dir "~/sync/org/attachments")
(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")
Task Management
See this blog post for my Org-Mode workflow for task management.
(setq org-enforce-todo-dependencies t)
(setq org-log-into-drawer t)
;; 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)
;; 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)))
(defun my/consult-org-agenda ()
(interactive)
(consult-org-agenda)
(org-tree-to-indirect-buffer))
(global-set-key (kbd "<f1>")
(lambda ()
(interactive)
(consult-org-heading nil '("~/sync/org/todo-personal.org"))))
(global-set-key (kbd "<f2>")
(lambda ()
(interactive)
(consult-org-heading nil '("~/sync/org/todo-work.org"))))
(global-set-key (kbd "<f3>")
(lambda ()
(interactive)
(consult-org-heading nil '("~/sync/org/notes.org"))))
(defun my/consult-ripgrep-org-directory ()
(interactive)
;; Add "--no-ignore-vcs" to the rg command so todo.org could be searched.
(let ((consult-ripgrep-args
"rg --null --line-buffered --color=never --max-columns=1000 --path-separator / --smart-case --no-heading --line-number --no-ignore-vcs ."))
(consult-ripgrep "~/sync/org" "")))
(global-set-key (kbd "C-S-f") 'my/consult-ripgrep-org-directory)
Org Breadcrumb
(defun ndk/heading-title ()
"Get the heading title."
(save-excursion
(if (not (org-at-heading-p))
(org-previous-visible-heading 1))
(org-element-property :title (org-element-at-point))))
(defun ndk/org-breadcrumbs ()
"Get the chain of headings from the top level down
to the current heading."
(let ((breadcrumbs (org-format-outline-path
(org-get-outline-path)
(1- (frame-width))
nil " > "))
(title (ndk/heading-title)))
(if (string-empty-p breadcrumbs)
title
(format "%s" breadcrumbs))))
(defun ndk/set-header-line-format()
(setq header-line-format '(:eval (ndk/org-breadcrumbs))))
(add-hook 'org-mode-hook #'ndk/set-header-line-format)
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)))
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)
Images
(setq org-startup-with-inline-images t)
;; allows overriding the image width
(setq org-image-actual-width nil)
Programming
Magit
There are multiple key-binding to trigger magit-status
:
C-x g
- The default keybinding from magit.
C-x p m
- The keybinding from project.el.
(use-package magit)
Diff Highlight
(use-package diff-hl
:init
(global-diff-hl-mode)
:hook
(magit-pre-refresh . diff-hl-magit-pre-refresh)
(magit-post-refresh . diff-hl-magit-post-refresh))
Terminal
(use-package vterm
:config
(define-key vterm-mode-map (kbd "<f1>") nil)
(define-key vterm-mode-map (kbd "<f2>") nil)
(define-key vterm-mode-map (kbd "<f3>") nil)
(define-key vterm-mode-map (kbd "<f4>") nil)
(define-key vterm-mode-map (kbd "<f5>") nil)
(define-key vterm-mode-map (kbd "<f6>") nil)
(define-key vterm-mode-map (kbd "<f7>") nil)
(define-key vterm-mode-map (kbd "<f8>") nil)
(define-key vterm-mode-map (kbd "<f9>") nil)
(define-key vterm-mode-map (kbd "<f10>") nil)
(define-key vterm-mode-map (kbd "<f11>") nil)
(define-key vterm-mode-map (kbd "<f12>") nil)
:custom
(vterm-shell "bash" "Set to bash instead of the default $SHELL so that vterm from TRAMP uses bash.")
(vterm-tramp-shells '(("docker" "/bin/sh") ("ssh" "/bin/bash")))
:hook
(vterm-mode . goto-address-mode))
(use-package vterm-toggle
:config
;; show vterm buffer in side window
(add-to-list 'display-buffer-alist
'("\\*vterm\\*"
(display-buffer-reuse-window display-buffer-in-side-window)
(side . bottom)
(dedicated . t)
(reusable-frames . visible) ;; depends on how I use Emacs / Emacs Client
(window-height . 0.5)))
:bind
("s-<return>" . vterm-toggle)
("C-`" . vterm-toggle)
("C-c t" . vterm-toggle))
Snippet Template with yasnippet
(use-package yasnippet
:config
(yas-global-mode)
:bind
("C-c y i" . yas-insert-snippet)
("C-c y n" . yas-new-snippet))
(use-package yasnippet-snippets)
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)
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)))
Company Mode
(use-package company)
LSP Mode
Documentation: https://emacs-lsp.github.io/lsp-mode/
(use-package lsp-mode
:init
(setq lsp-keymap-prefix "M-l")
:hook
(lsp-mode . lsp-enable-which-key-integration)
:commands lsp)
(use-package lsp-ui
:commands lsp-ui-mode
:bind
(:map lsp-ui-mode-map
([remap xref-find-references] . lsp-ui-peek-find-references)
([remap xref-find-definitions] . lsp-ui-peek-find-definitions)
("M-l i" . lsp-ui-imenu))
:config
(setq lsp-ui-peek-show-directory nil)
(setq lsp-ui-doc-enable t)
(setq lsp-ui-doc-header t)
(setq lsp-ui-doc-include-signature t)
(setq lsp-ui-doc-border (face-foreground 'default))
(setq lsp-ui-sideline-enable t)
(setq lsp-ui-sideline-show-code-actions t)
(setq lsp-ui-sideline-show-diagnostics nil)
(setq lsp-ui-sideline-show-hover nil)
(setq lsp-ui-sideline-delay 0.1))
(use-package lsp-treemacs)
C++ Setup
https://emacs-lsp.github.io/lsp-mode/page/lsp-clangd/
(use-package lsp-mode
:hook (c++-mode . lsp))
Python Setup
https://emacs-lsp.github.io/lsp-python-ms/
(use-package lsp-python-ms
:init (setq lsp-python-ms-auto-install-server t)
:hook (python-mode . (lambda ()
(require 'lsp-python-ms)
(lsp)))) ; or lsp-deferred
;; Use (pyvenv-*) to manage the Python virtual environment
(use-package pyvenv)
Caddyfile Mode
(use-package caddyfile-mode
:ensure t
:mode (("Caddyfile\\'" . caddyfile-mode)))
Competitive Programming
Auto Insert the C++ Template
;; *NOTE* Trailing slash important
(setq auto-insert-directory "~/.emacs.d/auto-insert/")
(define-auto-insert "\.cpp" "template.cpp")
(auto-insert-mode)
Compile and Run
See https://codeforces.com/blog/entry/101292.
(defun my/compileandrun()
(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/compileandrun)
Project Management
Project
I particularly love the handy projectile
function projectile-find-file-in-known-project
and adding that function to project.el
below.
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)
(setq project-switch-use-entire-map t)
Full Text Search (F
) 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 "F") #'my/consult-ripgrep-current-project)
Find File (a
) in All Known Project
(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)
(file (funcall project-read-file-name-function
"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)
Treemacs
First of all, press ?
to summon the helpful hydra.
The treemacs-follow-mode
is a little annoying to me. I turn it off and always use treemacs-find-file
for focusing.
(defun my/treemacs-find-file ()
(interactive)
(treemacs-find-file)
(treemacs-select-window))
;; Optimize Treemacs for note-taking with Org-Roam.
;; (with-eval-after-load 'treemacs
;; (defun my/trim-roam-prefix (name)
;; (if (string-prefix-p "20" name)
;; (substring name 9) name))
;; (defun my/treemacs-file-name-transformer (name)
;; (let ((trimmed (my/trim-roam-prefix name)))
;; (if (string-suffix-p ".org" trimmed)
;; (substring trimmed 0 -4) trimmed)))
;; (setq treemacs-file-name-transformer #'my/treemacs-file-name-transformer)
;; (defun treemacs-ignore-org-hidden-files (filename absolute-path)
;; (and (string-match-p (regexp-quote "/sync/org") absolute-path)
;; (string-prefix-p "." filename)))
;; (add-to-list 'treemacs-ignored-file-predicates #'treemacs-ignore-org-hidden-files)
;; (defun treemacs-ignore-org-archive (filename absolute-path)
;; (and (string-match-p (regexp-quote "/sync/org") absolute-path)
;; (string-suffix-p ".org_archive" filename)))
;; (add-to-list 'treemacs-ignored-file-predicates #'treemacs-ignore-org-archive))
(use-package treemacs
:config
(treemacs-follow-mode -1)
:custom
(treemacs-width 40)
(treemacs-width-is-initially-locked nil)
(treemacs-sorting 'mod-time-desc "Optimize for note taking with mod-time-desc.")
:bind
("<f9>" . treemacs)
("C-<f9>" . my/treemacs-find-file)
("M-0" . treemacs-select-window))
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")
:custom
(rime-show-candidate 'posframe)
(rime-posframe-style 'vertical))
CJK Word Wrap
Emacs 28 adds better word wrap / line break support for CJK. See Emacs 28 新特性介绍.
(setq word-wrap-by-category t)
Better CJK Font
cnfonts is a tool for configuring the fonts used in Emacs.
(use-package cnfonts
:init
(setq cnfonts-profiles '("hack"))
(setq cnfonts-default-fontsize 10)
:config
(cnfonts-mode 1))
File Management
Dirvish
(use-package dirvish
:init
(dirvish-override-dired-mode)
:custom
(dirvish-quick-access-entries ; It's a custom option, `setq' won't work
'(("o" "~/sync/org" "Org")
("b" "~/sync/blog" "Blog")))
:config
(setq dirvish-attributes '(vc-state subtree-state all-the-icons collapse git-msg file-time file-size))
:bind (
:map dirvish-mode-map ; Dirvish inherits `dired-mode-map'
("a" . dirvish-quick-access)
("f" . dirvish-file-info-menu)
("y" . dirvish-yank-menu)
("N" . dirvish-narrow)
("^" . dirvish-history-last)
("h" . dirvish-history-jump) ; remapped `describe-mode'
("s" . dirvish-quicksort) ; remapped `dired-sort-toggle-or-edit'
("v" . dirvish-vc-menu) ; remapped `dired-view-file'
("TAB" . dirvish-subtree-toggle)
("M-f" . dirvish-history-go-forward)
("M-b" . dirvish-history-go-backward)
("M-l" . dirvish-ls-switches-menu)
("M-m" . dirvish-mark-menu)
("M-t" . dirvish-layout-toggle)
("M-s" . dirvish-setup-menu)
("M-e" . dirvish-emerge-menu)
("M-j" . dirvish-fd-jump)))
Go (Baduk)
(use-package igo-org
:straight (igo-org :type git :host github :repo "misohena/el-igo")
:after org
:config
(igo-org-setup))
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
(pinentry-start))