My Emacs Configuration

This emacs configuration is written in org-mode. The source code is available at: https://github.com/meiyopeng/dot-config/blob/master/emacs/init.org

Join the [Church of Emacs](http://www.stallman.org/saint.html).
Hold the holy `M-x butterfly`.
Unleash the powers of the butterfly.

Emacs saves you time when you work, and takes it back when you play with it.

init.el

Use M-x org-babel-tangle or C-c C-v t to manually extract lisp code.

Personal Information

name and mail address

(setq user-full-name "Meiyo Peng"
      user-mail-address "[email protected]")

Id

(setq my-gmail-address "[email protected]")
(setq my-irc-nick "meiyopeng")

Bootstrap

Enforce minimum Emacs version

(let ((min-version "26.0.50"))
  (when (version< emacs-version min-version)
    (error "Gnu Emacs %s or newer is required" min-version)))
(setq load-prefer-newer t)

Emacs server

Detect existing emacs server and switch to it if possible

(require 'server)

(defun my-server-shunt ()
  "Shunts to emacsclient"
  (let ((args (append '("emacsclient" "-a" "\"\"" "-n")
                      (cdr command-line-args))))
    (shell-command (substring (format "%S" args) 1 -1))
    (kill-emacs)))

;; Keep only one Emacs server instance
(if (server-running-p)
    (if (daemonp)
        (error "Another running emacs server detected, abort")
      (my-server-shunt))
  (server-start))

Require common lisp

(require 'cl)

XDG specification

Define XDG directories

(require 'xdg)

(defvar user-emacs-config-dir
  (expand-file-name "emacs" (xdg-config-home)))

(defvar user-emacs-data-dir
  (expand-file-name "emacs" (xdg-data-home)))

(defvar user-emacs-cache-dir
  (expand-file-name "emacs" (xdg-cache-home)))

Package manager

(require 'package)
(setq package-archives '(("gnu" . "https://elpa.gnu.org/packages/")))
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))
(add-to-list 'package-archives '("org" . "https://orgmode.org/elpa/"))
(package-initialize)

Bootstrap use-package

(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (package-install 'use-package))

(eval-when-compile
  (require 'use-package))
(require 'bind-key)

(setq use-package-verbose t)

;; Disable lazy loading in daemon mode
(if (daemonp)
    (setq use-package-always-demand t))

custom.el

Variables configured via the interactive ‘customize’ interface

(setq custom-file (expand-file-name "custom.el" user-emacs-directory))
(load custom-file 'noerror)

Core

Emacs built-in features

environment

Determine operating system type

(defconst *os-is-gnu* (eq system-type 'gnu/linux))
(defconst *os-is-mac* (eq system-type 'darwin))
(defconst *os-is-windows* (eq system-type 'windows-nt))

Language

(set-language-environment "UTF-8")

Locale

set-locale-environment changes the default coding system, so it should be called before setting coding system.

(if *os-is-gnu*
    (set-locale-environment "en_US.UTF-8"))
(if *os-is-mac*
    (set-locale-environment "en_US.UTF-8"))
(if *os-is-windows*
    (set-locale-environment "ENU"))

Encoding

;;(set-default-coding-systems 'utf-8-unix)
(prefer-coding-system 'utf-8-unix)

Use hexadecimal instead of octal for quoted-insert (C-q).

(setq read-quoted-char-radix 16)

Font

(defun my-font-available-p (font)
  "Detect if a font is available"
  (if (find-font (font-spec :family font))
      t
    nil))

(defun my-set-font (font-list &optional font-size)
  "Set default font to the first available font in FONT-LIST"
  (let ((font (cl-find-if #'my-font-available-p font-list)))

    (if (null font)
        (user-error "No font is available in FONT-LIST"))

    (message "Set default font to %s" font)
    (set-face-font 'default
                   (font-spec :family font :size font-size))))

My preferred fonts

(when (display-graphic-p)
  (my-set-font '("DejaVu Sans Mono" "Menlo" "Consolas" "Monospace")))

basic interface

*scratch* buffer’s default content

(setq initial-scratch-message nil)

hide all kinds of bars

(menu-bar-mode -1)
(if (fboundp 'tool-bar-mode)
    (tool-bar-mode -1))
(if (fboundp 'scroll-bar-mode)
    (scroll-bar-mode -1))

mode line

(line-number-mode t)
(column-number-mode t)

(size-indication-mode t)

ring

(setq ring-bell-function 'ignore)

buffer name

(require 'uniquify)
(setq uniquify-buffer-name-style 'forward)
(setq uniquify-separator "/")
(setq uniquify-after-kill-buffer-p t)
(setq uniquify-ignore-buffers-re "^\\*")

frame name

;; show either a file or a buffer name
(setq frame-title-format
      '("" invocation-name " - "
        (:eval (if (buffer-file-name)
                   (abbreviate-file-name (buffer-file-name))
                 "%b"))))

key bindings

yes-or-no-p

(defalias 'yes-or-no-p 'y-or-n-p)

bind C-x k to kill-this-buffer

(global-set-key (kbd "C-x k") 'kill-this-buffer)

expand text

(global-set-key (kbd "M-/") 'hippie-expand)

upcase or downcase text

(global-set-key (kbd "M-u") 'upcase-dwim)
(global-set-key (kbd "M-l") 'downcase-dwim)

shell

(global-set-key (kbd "C-c s") 'eshell)

editing

fill column

(setq-default fill-column 80)

final new line

(setq require-final-newline t)

delete the selection with a key press

(delete-selection-mode t)

smart tab key behavior, indent or complete

(setq tab-always-indent 'complete)

indentation

;; don't use tabs to indent
(setq-default indent-tabs-mode nil)

(setq-default tab-width 8)

Revert buffers automatically when underlying files are changed externally

(global-auto-revert-mode t)

Automatically save buffers to file when losing focus

(defun my-save-buffers ()
  "Save all file-visiting buffers"
  (save-some-buffers t nil))

(add-hook 'focus-out-hook 'my-save-buffers)

Automatically make a shell script executable on save

(add-hook 'after-save-hook
          'executable-make-buffer-file-executable-if-script-p)

highlight

(blink-cursor-mode -1)

;; highlight the current line
(global-hl-line-mode 1)

;; highlight matching parentheses when the point is on them
(show-paren-mode t)

(setq blink-matching-paren nil)

whitespace-mode

(require 'whitespace)
(setq whitespace-style '(face empty trailing lines-tail indentation))
(setq whitespace-line-column 80)

(defun my-whitespace-mode-setup ()
  (whitespace-mode 1)
  (add-hook 'before-save-hook 'whitespace-cleanup nil t))

basic major modes

text-mode

(add-hook 'text-mode-hook 'auto-fill-mode)
(add-hook 'text-mode-hook 'my-whitespace-mode-setup)

prog-mode

(add-hook 'prog-mode-hook 'abbrev-mode)
(add-hook 'prog-mode-hook 'my-whitespace-mode-setup)

(defun my-prog-mode-setup ()
  (which-function-mode 1)

  (setq-local comment-auto-fill-only-comments t)
  (auto-fill-mode 1)

  ;; highlight a bunch of well known comment annotations
  (font-lock-add-keywords
   nil
   '(("\\<\\(\\(FIX\\(ME\\)?\\|TODO\\|OPTIMIZE\\|HACK\\|REFACTOR\\):\\)"
      1 font-lock-warning-face t))))

(add-hook 'prog-mode-hook 'my-prog-mode-setup)

tramp

(require 'tramp)
(setq tramp-default-method "ssh")

dired

(setq dired-recursive-copies 'always)
(setq dired-recursive-deletes 'always)
(require 'dired-x)

bookmark

(require 'bookmark)
(setq bookmark-save-flag 1)

internet

Don’t send anything in HTTP header field

(setq url-privacy-level 'paranoid)

Proxy

SOCKS 5 proxy

(setq url-gateway-method 'socks)
(setq socks-server '("Default server" "localhost" 1080 5))

HTTP proxy

(setq url-proxy-services
      '(("no_proxy" . "^\\(localhost\\|10\\..*\\|192\\.168\\..*\\)")
        ("http" . "localhost:1081")
        ("https" . "localhost:1081")))

Browser

eww

(global-set-key (kbd "C-c w") 'eww)
(global-set-key (kbd "C-c b") 'eww-list-bookmarks)

Email

message mode

;; Turn on PGP
(add-hook 'message-mode-hook 'epa-mail-mode)

;; Message signature
(setq message-signature-directory
      (expand-file-name "signature" (xdg-config-home)))
(setq message-signature-file "personal")

;; Don't keep message buffer after sending a message.
(setq message-kill-buffer-on-exit t)

SMTP

(setq message-send-mail-function 'message-smtpmail-send-it)

(setq smtpmail-smtp-server "smtp.mailgun.com"
      smtpmail-stream-type 'ssl  ;; StartTLS is evil.
      smtpmail-smtp-service 465)

sendmail

(setq message-send-mail-function 'message-send-mail-with-sendmail)

;; Use the "From:" address in mail header as envelope-from address.
(setq mail-specify-envelope-from t
      mail-envelope-from 'header)
(setq message-sendmail-envelope-from 'header)

msmtp

(setq sendmail-program "msmtp")

security

GPG

Query passphrase through the minibuffer, instead of the pinentry program

(setq epg-pinentry-mode 'loopback)

auth-source

(setq auth-sources
      (list (expand-file-name "auth/netrc.gpg" (xdg-data-home))))

Get secret from auth-source

(cl-defun my-get-secret (&rest spec &key domain port user &allow-other-keys)
  (let ((record (nth 0 (auth-source-search :max 1
                                           :host domain
                                           :port port
                                           :user user
                                           :require '(:secret)))))
    (if record
        (let ((secret (plist-get record :secret)))
          (if (functionp secret)
              (funcall secret)
            secret))
      nil)))

session

desktop

(setq desktop-auto-save-timeout 600)
(desktop-save-mode t)

recent files

(require 'recentf)
(setq recentf-auto-cleanup 'never)
(setq recentf-exclude
      (mapcar 'expand-file-name
              (list "/gnu" "/tmp" "/ssh:" "~/.cache" package-user-dir)))
(recentf-mode 1)

minibuffer history

(savehist-mode 1)

auto-save

(setq auto-save-list-file-prefix
      (expand-file-name "auto-save-list/" user-emacs-cache-dir))

backup

(let ((backup-dir (expand-file-name "backup" user-emacs-cache-dir)))
  (setq-default backup-directory-alist `((".*" . ,backup-dir))))

Theme

theme

(use-package zenburn-theme
  :ensure t
  :config
  (load-theme 'zenburn))

transparency

alpha ‘( . )

(set-frame-parameter (selected-frame) 'alpha '(95 . 60))
(add-to-list 'default-frame-alist '(alpha . (98 . 80)))

mode line

(use-package diminish
  :ensure t
  :config
  (diminish 'abbrev-mode)
  (diminish 'auto-fill-function)
  (diminish 'auto-revert-mode)
  (diminish 'eldoc-mode)
  (diminish 'whitespace-mode))

cursor

highlight the cursor whenever the window scrolls

(use-package beacon
  :ensure t
  :diminish beacon-mode
  :config
  (beacon-mode t))

Utilities

helm

(use-package helm
  :ensure t
  :defer 3
  :diminish helm-mode
  :bind-keymap ("C-c h" . helm-command-map)
  :bind (("C-c f" . helm-recentf)
         ("C-h a" . helm-apropos)
         ("C-x b" . helm-mini)
         ("C-x C-b" . helm-buffers-list)
         ("C-x C-d" . helm-browse-project)
         ("C-x C-f" . helm-find-files)
         ("M-x" . helm-M-x)
         ("M-y" . helm-show-kill-ring)
         ("M-s o" . helm-occur)
         :map helm-command-map
         ("g" . helm-do-grep-rg)
         ("M-g g" . helm-do-grep-rg))
  :init
  (defalias 'helm-do-grep-rg 'helm-do-grep-ag)
  :config
  (require 'helm-config)
  (helm-mode 1)

  (setq helm-move-to-line-cycle-in-source t)

  ;; fuzzy matching
  (setq helm-mode-fuzzy-match t)
  (setq helm-completion-in-region-fuzzy-match t)
  (setq helm-M-x-fuzzy-match t
        helm-buffers-fuzzy-matching t
        helm-recentf-fuzzy-match t)

  (add-to-list 'helm-mini-default-sources 'helm-source-bookmarks 'append)

  (setq helm-ff-file-name-history-use-recentf t)
  (setq helm-ff-skip-boring-files t)

  ;; ripgrep
  (setq helm-grep-ag-command "rg --color=always --colors 'match:fg:black' --colors 'match:bg:yellow' --smart-case --no-heading --line-number %s %s %s")
  (setq helm-grep-ag-pipe-cmd-switches '("--colors 'match:fg:black'" "--colors 'match:bg:yellow'"))
  )

helm-ls-git

(use-package helm-ls-git
  :ensure t
  :after helm)

projectile

(use-package projectile
  :ensure t
  :defer 10
  :diminish projectile-mode
  :bind-keymap (("C-c p" . projectile-command-map)
                ("s-p" . projectile-command-map))
  :config
  (projectile-global-mode t)
  (setq projectile-project-search-path '("~/Projects"))

  ;; Search method
  (setq projectile-indexing-method 'alien)
  (setq projectile-generic-command "fd . -0")
  (setq projectile-git-command "fd . -0"))

(use-package helm-projectile
  :ensure t
  :after (helm projectile)
  :config
  (helm-projectile-on))

file explorer

(use-package dired-sidebar
  :ensure t
  :commands (dired-sidebar-toggle-sidebar))

crux

(use-package crux
  :ensure t
  :bind (("C-a" . crux-move-beginning-of-line)
         ("C-c d" . crux-duplicate-current-line-or-region)
         ("C-c D" . crux-delete-file-and-buffer)
         ("C-c e" . crux-eval-and-replace)
         ("C-c I" . crux-find-user-init-file)
         ("C-c o o" . crux-open-with)
         ("C-c o r" . crux-sudo-edit)
         ("C-c r n" . crux-rename-file-and-buffer)
         ("C-c TAB" . crux-indent-defun)
         ("C-x K" . crux-kill-other-buffers)
         ("C-^" . crux-top-join-line)
         ("C-<BACKSPACE>" . crux-kill-line-backwards)
         ("C-S-<BACKSPACE>" . crux-kill-whole-line)))

key map

which-key

(use-package which-key
  :ensure t
  :defer 10
  :diminish which-key-mode
  :config
  (which-key-mode 1))

discover-my-major

(use-package discover-my-major
  :ensure t
  :commands (discover-my-major discover-my-mode)
  :bind ("C-h m" . discover-my-major))

undo-tree

(use-package undo-tree
  :ensure t
  :diminish undo-tree-mode
  :bind ("C-x u" . undo-tree-visualize)
  :config
  (global-undo-tree-mode t))

move cursor

avy

jump to visible text using a char-based decision tree

(use-package avy
  :ensure t
  :bind (("C-c j" . avy-goto-char-timer)
         ("M-g g" . avy-goto-line))
  :config
  (setq avy-background t))

ace-window

select a window

(use-package ace-window
  :ensure t
  :bind ("C-x o" . ace-window)
  :config
  (setq aw-scope 'frame))

multiple cursors

(use-package multiple-cursors
  :ensure t
  :bind (("C-|" . mc/edit-lines)
         ("C->" . mc/mark-next-like-this)
         ("C-<" . mc/mark-previous-like-this)
         ("C-S-<mouse-1>" . mc/add-cursor-on-click)))

anzu-mode enhances isearch & query-replace by showing total matches and current match position in the mode-line

(use-package anzu
  :ensure t
  :diminish anzu-mode
  :bind (("M-%" . anzu-query-replace)
         ("C-M-%" . anzu-query-replace-regexp))
  :config
  (global-anzu-mode t))

alert

(use-package alert
  :ensure nil
  :config
  (setq alert-default-style 'libnotify))

version control

Git

magit

(use-package magit
  :ensure t
  :mode ("/\\(\
\\(\\(COMMIT\\|NOTES\\|PULLREQ\\|TAG\\)_EDIT\\|MERGE_\\|\\)MSG\
\\|\\(BRANCH\\|EDIT\\)_DESCRIPTION\\)\\'" . git-commit-mode)
  :bind ("C-x g" . magit-status))

git modes

(use-package gitconfig-mode
  :ensure t
  :mode ("/\\.gitconfig\\'" "/\\.git/config\\'" "/git/config\\'"
         "/\\.gitmodules\\'"))

(use-package gitignore-mode
  :ensure t
  :mode ("/\\.gitignore\\'" "/\\.git/info/exclude\\'" "/git/ignore\\'"))

diff-hl

(use-package diff-hl
  :ensure t
  :defer 10
  :config
  (global-diff-hl-mode t)
  (add-hook 'dired-mode-hook 'diff-hl-dired-mode)
  (add-hook 'magit-post-refresh-hook 'diff-hl-magit-post-refresh))

completion

(use-package company
  :ensure t
  :defer 10
  :diminish company-mode
  :config
  (global-company-mode 1))

spell checking

flyspell

(use-package flyspell
  :ensure t
  :defer 10
  :diminish flyspell-mode
  :preface
  (defvar my-enable-flyspell nil)
  (cond
   ((executable-find "aspell")
    (setq ispell-program-name "aspell")
    (setq my-enable-flyspell t))
   ((executable-find "hunspell")
    (setq ispell-program-name "hunspell")
    (setq ispell-dictionary "en_US")
    (setq my-enable-flyspell t))
   (t
    (message "Neither aspell nor hunspell found")))
  :if my-enable-flyspell
  :hook ((text-mode . flyspell-mode)
         (prog-mode . flyspell-prog-mode)))

Programming

parenthesis

smart parens

(use-package smartparens
  :ensure t
  :defer 10
  :diminish smartparens-mode
  :hook (prog-mode . smartparens-strict-mode)
  :config
  (require 'smartparens-config)
  (show-smartparens-global-mode 1))

colorful parens

(use-package rainbow-delimiters
  :ensure t
  :hook (prog-mode . rainbow-delimiters-mode))

flycheck

(use-package flycheck
  :ensure t
  :diminish flycheck-mode
  :hook (prog-mode . flycheck-mode)
  :config
  (setq flycheck-display-errors-function
        'flycheck-display-error-messages-unless-error-list))

language server protocol

lsp-mode

(use-package lsp-mode
  :ensure t
  :commands (lsp lsp-mode))

lsp-ui

(use-package lsp-ui
  :ensure t
  :after (lsp-mode)
  :commands (lsp-ui-mode))

company-lsp

(use-package company-lsp
  :ensure t
  :after (company lsp-mode)
  :commands (company-lsp))

debug

(use-package dap-mode
  :ensure t
  :after (lsp-mode)
  :commands (dap-mode dap-ui-mode))

yasnippet

(use-package yasnippet
  :ensure t
  :diminish yas-minor-mode
  :config
  (add-to-list 'yas-snippet-dirs "~/Projects/guix/etc/snippets")
  (yas-global-mode 1))

Lisp

indentation

(use-package aggressive-indent
  :ensure t
  :diminish aggressive-indent-mode
  :hook ((emacs-lisp-mode scheme-mode) . aggressive-indent-mode))

Scheme

geiser

(use-package geiser
  :ensure t
  :hook (scheme-mode . geiser-mode--maybe-activate)
  :config
  (setq geiser-active-implementations '(guile))
  (setq geiser-mode-start-repl-p t)
  (setq geiser-repl-history-filename
        (expand-file-name "geiser_history" user-emacs-directory)))

guix

(use-package guix
  :ensure t
  ;; :hook (scheme-mode . guix-devel-mode)
  )

Python

Prefer Python 3

(setq python-shell-interpreter "python3")

python mode

(defun my-python-mode-setup ()
  (add-hook 'post-self-insert-hook
            'electric-layout-post-self-insert-function
            nil t))

(add-hook 'python-mode-hook 'my-python-mode-setup)

anaconda-mode

(use-package anaconda-mode
  :ensure t
  :hook ((python-mode . anaconda-mode)
         (python-mode . anaconda-eldoc-mode)))

(use-package company-anaconda
  :ensure t
  :after (company anaconda-mode)
  :config
  (push 'company-anaconda company-backends))

Language Server Protocol

(add-hook 'python-mode-hook 'lsp)

Install python language server

pip3 install python-language-server[all]

Go

(use-package go-mode
  :ensure t
  :mode ("\\.go\\'" . go-mode))

(use-package go-eldoc
  :ensure t
  :after (go-mode)
  :hook (go-mode . go-eldoc-setup))
(defun my-go-mode-setup ()
  (add-hook 'before-save-hook 'gofmt-before-save nil t))

(add-hook 'go-mode-hook 'my-go-mode-setup)

Language Server Protocol

(add-hook 'go-mode-hook 'lsp)

Install go language server

go get -u golang.org/x/tools/cmd/gopls

C

(setq c-default-style "linux")
(setq-default c-basic-offset 4)

Language Server Protocol

(add-hook 'c-mode-hook 'lsp)
(add-hook 'c++-mode-hook 'lsp)

Install C language server

# Install clangd.

clang-format

(use-package clang-format
  :ensure t
  :commands (clang-format-buffer))

(defun clang-format-buffer-smart ()
  "Reformat buffer if .clang-format exists in the projectile root"
  (when (f-exists? (expand-file-name ".clang-format" (projectile-project-root)))
    (clang-format-buffer)))

(defun my-c-mode-setup ()
  (add-hook 'before-save-hook 'clang-format-buffer-smart nil t))

(add-hook 'c-mode-hook 'my-c-mode-setup)
(add-hook 'c++-mode-hook 'my-c-mode-setup)

Java

Language Server Protocol

(use-package lsp-java
  :ensure nil
  :after lsp
  :hook (java-mode . lsp))

JavaScript

Install JavaScript language server

npm i -g typescript-language-server
npm i -g typescript

C

csharp-mode

(use-package csharp-mode
  :ensure nil
  :mode ("\\.cs\\'" . csharp-mode)
  :config
  (defun my-csharp-mode-setup ()
    (c-set-style "c#"))

  (add-hook 'csharp-mode-hook 'my-csharp-mode-setup))

OmniSharp

(use-package omnisharp
  :ensure nil
  :hook (csharp-mode . omnisharp-mode))

Install OmniSharp server: M-x omnisharp-install-server

fish shell

(use-package fish-mode
  :ensure t
  :mode ("\\.fish\\'" . fish-mode)
  :interpreter ("fish"))

Markup Languages

Org Mode

org

(use-package org
  :ensure org-plus-contrib
  :defer 10
  :mode ("\\.org\\'" . org-mode)
  :bind (("C-c a" . org-agenda)
         ("C-c c" . org-capture)
         ("C-c l" . org-store-link)
         ("C-c C-," . org-insert-structure-template))
  :config
  (setq org-directory "~/Sync/Org")
  (setq org-agenda-files (list org-directory))
  (setq org-default-notes-file
        (expand-file-name "Organizer.org" org-directory))
  (setq my-org-inbox-file (expand-file-name "Inbox.org" org-directory))

  (setq org-catch-invisible-edits 'show)
  (setq org-id-track-globally nil)      ; Do not store org IDs on disk.
  (setq org-use-sub-superscripts '{})

  ;;; org-agenda
  (setq org-agenda-default-appointment-duration 60)
  (setq org-agenda-compact-blocks t)
  (setq org-agenda-span 'month)
  (setq org-agenda-start-on-weekday nil)

  ;;; org-todo
  (setq org-log-done 'time)
  (setq org-log-into-drawer t)
  (setq org-todo-keywords
        '((sequence "TODO(t)" "NEXT(n)" "|" "DONE(d!/!)" "CANCELLED([email protected]/!)")))
  (setq org-todo-repeat-to-state "NEXT")
  (setq org-todo-keyword-faces '(("NEXT" :inherit warning)))

  ;;; org-tag
  (setq org-fast-tag-selection-single-key 'expert)
  (setq org-tags-column -80)            ; Align right edge to 80th column.

  ;;; org-capture
  (setq org-capture-templates
        `(("i" "inbox"
           entry (file my-org-inbox-file)
           "* %?\n")
          ("j" "journal"
           entry (file+olp org-default-notes-file "Journal")
           "* %u\n%?\n")
          ("t" "todo"
           entry (file+olp org-default-notes-file "Agenda")
           "* TODO %?\n  :PROPERTIES:\n  :Captured_at: %U\n  :END:\n")))

  (add-to-list 'org-structure-template-alist
               '("semacs" . "src emacs-lisp") t)
  (add-to-list 'org-structure-template-alist
               '("sscheme" . "src scheme") t)
  (add-to-list 'org-structure-template-alist
               '("sshell" . "src sh") t)

  ;;; org-clock
  ;; Persist the running clock and all clock history
  (org-clock-persistence-insinuate)
  (setq org-clock-persist t)
  (setq org-clock-in-resume t)
  ;; Save clock data and notes in drawer
  (setq org-clock-into-drawer t)
  ;; Remove the clock line when result time is zero
  (setq org-clock-out-remove-zero-time-clocks t)

  ;;; org-babel
  (org-babel-do-load-languages
   'org-babel-load-languages
   '((dot . t)
     (latex . t)
     (ledger . t)
     (python . t)
     (scheme . t)
     (shell . t)
     (sql . t)))

  ;; Prefer Python 3
  (setq org-babel-python-command "python3")

  ;; Disable emacs-lisp-checker for org-src-mode
  (add-hook 'org-src-mode-hook
            (lambda ()
              (setq-local flycheck-disabled-checkers
                          '(emacs-lisp-checkdoc))))

  ;;; org-export
  (setq org-export-exclude-tags '("noexport" "private"))
  (setq org-export-with-section-numbers nil)
  (setq org-export-with-sub-superscripts '{})
  (setq org-export-with-toc nil)

  ;;; org-html
  (setq org-html-doctype "html5")
  (setq org-html-html5-fancy-p t)
  (setq org-html-validation-link nil)

  ;;; org-latex
  (add-to-list 'org-latex-packages-alist '("" "color"))
  (add-to-list 'org-latex-packages-alist '("" "listings"))
  (setq org-latex-listings t
        org-latex-listings-options '(("basicstyle" "\\small")
                                     ("frame" "single")))

  ;;; org-icalendar
  (setq org-icalendar-alarm-time 60)    ; 60 minutes before the event.
  (setq org-icalendar-combined-agenda-file
        (expand-file-name "agenda.ics" org-directory))
  (setq org-icalendar-exclude-tags
        (append org-export-exclude-tags '("archive" "journal")))
  ;; Include tasks that are not in DONE state.
  (setq org-icalendar-include-todo t)
  ;; Include scheduled and deadline events.
  (setq org-icalendar-use-scheduled
        '(event-if-todo event-if-not-todo todo-start))
  (setq org-icalendar-use-deadline
        '(event-if-todo event-if-not-todo todo-due))
  ;; Whether to make events from plain time stamps.
  (setq org-icalendar-with-timestamps 'active))

org-alert

Notifications for org agenda items

(use-package org-alert
  :ensure t
  :defer 20
  :config
  (setq org-alert-interval 600)
  (org-alert-enable))

ox-hugo

(use-package ox-hugo
  :ensure t
  :after ox)

Add UUID to all org headlines

(defun my-add-uuid-to-org-headlines-in-buffer ()
  "Add ID properties to all headlines in the current buffer."
  (interactive)
  (org-map-entries 'org-id-get-create))

HTML

htmlize – Convert buffer text and decorations to HTML

(use-package htmlize
  :ensure t
  :defer 10)

rainbow mode

(use-package rainbow-mode
  :ensure t
  :hook ((html-mode css-mode) . rainbow-mode))

Install HTML language server

npm install -g vscode-html-languageserver-bin

Install CSS language server

npm install -g vscode-css-languageserver-bin

YAML

(use-package yaml-mode
  :ensure t
  :mode ("\\.yaml\\'" "\\.yml\\'"))

CSV

(use-package csv-mode
  :ensure t
  :mode ("\\.csv\\'" . csv-mode))

Ledger

(use-package ledger-mode
  :ensure nil
  :mode ("\\.ledger\\'" . ledger-mode)
  :config
  (use-package flycheck-ledger
    :ensure t))

TeX

auctex

(use-package auctex
  :ensure nil
  :mode ("\\.tex\\'" . latex-mode)
  :config
  (setq TeX-auto-save t
        TeX-parse-self t
        TeX-PDF-mode t)
  (setq-default TeX-master nil)

  (defun my-latex-mode-setup ()
    (LaTeX-preview-setup)
    (LaTeX-math-mode))

  (add-hook 'latex-mode-hook 'my-latex-mode-setup)

  (use-package company-auctex
    :ensure t
    :after (company auctex)
    :hook (latex-mode . company-auctex-init))

  (use-package reftex
    :ensure t
    :hook (latex-mode . turn-on-reftex)
    :config
    (setq reftex-plug-into-AUCTeX t))
  )

Markdown

(use-package markdown-mode
  :ensure nil
  :mode (("\\.md\\'" . markdown-mode)
         ("\\.markdown\\'" . markdown-mode)
         ("README\\.md\\'" . gfm-mode)))

ob-http

(use-package ob-http
  :ensure t
  :after (ob)
  :mode ("\\.http\\'" . org-mode))

po-mode

po-mode is provided by gettext.

(use-package po-mode
  :ensure nil
  :mode ("\\.pot?\\'" . po-mode)
  :config
  ;; Do not wrap lines when editing msgstr
  (add-hook 'po-subedit-mode-hook
            (lambda ()
              (setq fill-column 1000)))

  ;; https://www.emacswiki.org/emacs/PoMode
  (defun po-wrap ()
    "Filter current po-mode buffer through `msgcat' tool to wrap all lines."
    (interactive)
    (if (eq major-mode 'po-mode)
        (let ((tmp-file (make-temp-file "po-wrap."))
              (tmp-buf (generate-new-buffer "*temp*")))
          (unwind-protect
              (progn
                (write-region (point-min) (point-max) tmp-file nil 1)
                (if (zerop
                     (call-process
                      "msgcat" nil tmp-buf t (shell-quote-argument tmp-file)))
                    (let ((saved (point))
                          (inhibit-read-only t))
                      (delete-region (point-min) (point-max))
                      (insert-buffer tmp-buf)
                      (goto-char (min saved (point-max))))
                  (with-current-buffer tmp-buf
                    (error (buffer-string)))))
            (kill-buffer tmp-buf)
            (delete-file tmp-file))))))

Internet

Gnus

(use-package gnus
  :commands (gnus)
  :config
  ;; Email servers
  (setq my--gnus-local '(nnmaildir "local"
                                   (directory "~/Mail")
                                   (get-new-mail nil)))
  ;; Usenet servers
  (setq my--gnus-gmane
        '(nntp "gmane"
               (nntp-address "news.gmane.org")
               (nntp-port-number 563)
               (nntp-open-connection-function nntp-open-tls-stream))
        my--gnus-aioe
        '(nntp "aioe"
               (nntp-address "nntp.aioe.org")
               (nntp-port-number 563)
               (nntp-open-connection-function nntp-open-tls-stream)))

  (setq gnus-select-method my--gnus-local)
  ;; (setq gnus-secondary-select-methods
  ;;       (list my--gnus-gmane my--gnus-aioe))
  )

mu4e

(setq mail-user-agent 'mu4e-user-agent)

(use-package mu4e
  :ensure nil
  :bind (("C-c m" . mu4e))
  :config

  (setq mu4e-confirm-quit nil)

  ;; Don't save message to the "sent" folder if IMAP takes care of this.
  ;; (setq mu4e-sent-messages-behavior 'delete)

  ;; Fetch email.
  (setq mu4e-get-mail-command "offlineimap")

  ;; Default context.
  (setq mu4e-maildir "~/Mail")
  (setq mu4e-drafts-folder "/drafts")
  (setq mu4e-refile-folder "/archive")
  (setq mu4e-sent-folder   "/sent")
  (setq mu4e-trash-folder  "/trash")

  (setq mu4e-maildir-shortcuts
        '(("/archive" . ?a)
          ("/drafts"  . ?d)
          ("/INBOX"   . ?i)
          ("/sent"    . ?s)
          ("/spam"    . ?j)
          ("/trash"   . ?t)))

  (add-to-list 'mu4e-headers-actions
               '("git apply mbox" . mu4e-action-git-apply-mbox) t)

  (add-to-list 'mu4e-view-actions
               '("git apply mbox" . mu4e-action-git-apply-mbox) t))

IRC

ERC

(use-package erc
  :commands (erc my-erc-start-or-switch)
  :config
  (setq erc-nick "meiyopeng")
  (setq erc-autojoin-channels-alist
        '((".*\\.freenode.net" "#emacs")))
  (erc-autojoin-mode t)

  ;; spell checking
  (erc-spelling-mode 1)

  ;; logging
  (setq erc-log-channels-directory
        (expand-file-name "erc" (xdg-data-home)))

  (setq erc-save-buffer-on-part t)

  ;; fallback to auth-source
  (setq erc-prompt-for-password nil)

  ;; Kill buffers for channels after /part
  (setq erc-kill-buffer-on-part t)
  ;; Kill buffers for private queries after quitting the server
  (setq erc-kill-queries-on-quit t)
  ;; Kill buffers for server messages after quitting the server
  (setq erc-kill-server-buffer-on-quit t)

  ;; open query buffers in the current window
  (setq erc-query-display 'buffer)

  (setq erc-auto-reconnect nil)

  (erc-track-mode t)
  (setq erc-track-exclude-types '("JOIN" "NICK" "PART" "QUIT" "MODE"
                                  "324" "329" "332" "333" "353" "477"))

  (defun my-erc-start-or-switch ()
    "Connect to ERC, or switch to last active buffer"
    (interactive)
    (if (get-buffer "irc.freenode.net:6697")
        (erc-track-switch-buffer 1)
      (when (y-or-n-p "Start ERC? ")
        (erc-tls :server "irc.freenode.net" :port 6697
                 :nick my-irc-nick)))))

RSS feed

elfeed

(use-package elfeed
  :ensure t
  :bind (("C-c n" . my-elfeed-open)
         :map elfeed-search-mode-map
         ("q" . my-elfeed-quit))
  :config
  (setq elfeed-db-directory
        (expand-file-name "elfeed" (xdg-data-home)))

  (use-package elfeed-org
    :ensure t
    :after (elfeed)
    :config
    (elfeed-org))

  (defun my-elfeed-open ()
    (interactive)
    (elfeed-db-load)
    (elfeed))

  (defun my-elfeed-quit ()
    (interactive)
    (elfeed-search-quit-window)
    (elfeed-db-unload)))

lookup Wikipedia

(require 'browse-url)

(defun my-lookup-wikipedia ()
  "Look up the word under cursor in Wikipedia.
If there is a text selection, use that."
  (interactive)
  (let (word)
    (setq word
          (if (use-region-p)
              (buffer-substring-no-properties (region-beginning) (region-end))
            (current-word)))
    (setq word (replace-regexp-in-string " " "_" word))
    (browse-url (concat "http://en.wikipedia.org/wiki/" word))))

lookup Wiktionary

(autoload 'ispell-get-word "ispell")

(defun my-lookup-wiktionary (word)
  (interactive (list (save-excursion (car (ispell-get-word nil)))))
  (browse-url (format "http://en.wiktionary.org/wiki/%s" word)))

(global-set-key (kbd "M-#") 'my-lookup-wiktionary)

Media

epub

(use-package nov
  :ensure nil
  :mode ("\\.epub\\'" . nov-mode))

pdf

(use-package pdf-tools
  :ensure nil
  :mode ("\\.pdf\\'" . pdf-view-mode)
  :config
  (pdf-loader-install))

Developer Tools

debbugs

(use-package debbugs
  :ensure nil
  :commands (debbugs-gnu
             debbugs-org
             debbugs-gnu-bugs
             debbugs-org-bugs
             debbugs-gnu-search
             debbugs-org-search)
  :config
  (setq debbugs-gnu-default-packages '("guix")))

Chinese compatibility hack

Chinese Font

To align Chinese characters with English characters vertically, the Chinese font should be rescaled.

chinese_font_size = 1.2 * english_font_zise
chinese_character_width = 2 * english_character_width
(defun my-set-chinese-font (font-list &optional font-size)
  "Set Chinese font to the first available font in FONT-LIST."
  (let ((chinese-font (cl-find-if #'my-font-available-p font-list)))

    (if (null chinese-font)
        (user-error "No font is available in FONT-LIST"))

    (message "Set Chinese font to %s" chinese-font)
    (dolist (charset '(han cjk-misc))
      (set-fontset-font t charset
                        (font-spec :family chinese-font :size font-size)))

    ;; Rescale Chinese fonts
    (setq face-font-rescale-alist
          '((".*WenQuanYi.*" . 1.2)
            (".*Heiti.*" . 1.2)
            (".*Yahei.*" . 1.2)))
    ))

My preferred fonts

(when (display-graphic-p)
  (my-set-chinese-font
   '("WenQuanYi Micro Hei" "WenQuanYi Zen Hei" "Heiti SC" "STHeiti" "Microsoft Yahei")))

Fix line break in Chinese paragraph

by zwz.github.io

(defun clear-single-linebreak-in-cjk-string (string)
  "clear single line-break between cjk characters that is usually soft
line-breaks"
  (let* ((cjk-char "[\u3000-\u303F]\\|[\u4E00-\u9FFF]\\|[\uFF01-\uFF5E]")
         (regexp (concat "\\(" cjk-char "\\)\n\\(" cjk-char "\\)"))
         (start (string-match regexp string)))
    (while start
      (setq string (replace-match "\\1\\2" nil nil string)
            start (string-match regexp string start))))
  string)

(defun ox-html-clear-single-linebreak-for-cjk (string backend info)
  (when (org-export-derived-backend-p backend 'html)
    (clear-single-linebreak-in-cjk-string string)))

(eval-after-load "ox"
  '(add-to-list 'org-export-filter-final-output-functions
                'ox-html-clear-single-linebreak-for-cjk))

local.init.el

Load an optional local init file

(load (expand-file-name "local.init.el" user-emacs-directory) 'noerror)