Introduction

Emacs outshines all other editing software in approximately the same way that the noonday sun does the stars. It is not just bigger and brighter; it simply makes everything else vanish.

– Neal Stephenson, In the Beginning was the Command Line (1998)

Install Emacs-plus With Homebrew

brew tap d12frosted/emacs-plus
brew install emacs-plus --with-native-comp --with-modern-vscode-icon
ln -s /usr/local/opt/emacs-plus@29/Emacs.app /Applications

Build Emacs on UbuntuLucius’s Workflow

git clone git://git.sv.gnu.org/emacs.git
sudo apt install build-essential libgtk-3-dev libgnutls28-dev libtiff5-dev libgif-dev libjpeg-dev libpng-dev libxpm-dev libncurses-dev texinfo autoconf
cd emacs
./autogen.sh
./configure
make -j8
sudo make install

telega 中需要 svg 和 webp 的支持

sudo apt-get install -y librsvg2-dev
sudo apt-get install webp

Ubuntu 上的 Dropbox 参考 Install - Dropbox,目前的情况是只有命令行启动的客户端才可以同步成功。

cd ~ && wget -O - "https://www.dropbox.com/download?plat=lnx.x86_64" | tar xzf -
~/.dropbox-dist/dropboxd

为了使用方便使用,进行了改键,通过下面的命令把 CAPS(66)LCTRL(37) 互换。

sudo gedit /us r/share/X11/xkb/keycodes/evdev

输入法安装 fcitx-rime,和 MacOS 上的配置通用,放在 ~/.config/fcitx/rime​。

sudo apt-get install fcitx-rime

Personal Settings

目前在 purcell 中就增加或修改了这些文件。

.
├── lisp
    ├── ligature.el
    ├── init-org.el
    ├── init-sis.el
    ├── init-rime.el
    ├── init-font.el
    ├── init-meow.el
    ├── beancount.el
    ├── init-elpa.el
    ├── init-local.el
    ├── init-latex.el
    ├── init-corfu.el
    ├── init-bibtex.el
    ├── init-themes.el
    ├── init-elfeed.el
    ├── init-telega.el
    ├── init-translate.el
    ├── init-org-modern.el
    ├── init-quick-menus.el
    ├── init-tree-sitter.el
    ├── init-org-enhance.el
    ├── telega-bridge-bot.el
    └── init-org-transclusion.el

ligature.el

GitHub - mickeynp/ligature.el: Display typographical ligatures in Emacs,这里是将其中的 ligature.el 下载到 .emacs.d/lisp/ 目录下,加上下列代码。

;; Enable the www ligature in every possible major mode
(ligature-set-ligatures 't '("www"))

;; Enable ligatures in programming modes
(ligature-set-ligatures 'prog-mode '("www" "**" "***" "**/" "*>" "*/" "\\\\" "\\\\\\" "{-" "::"
                                     ":::" ":=" "!!" "!=" "!==" "-}" "----" "-->" "->" "->>"
                                     "-<" "-<<" "-~" "#{" "#[" "##" "###" "####" "#(" "#?" "#_"
                                     "#_(" ".-" ".=" ".." "..<" "..." "?=" "??" ";;" "/*" "/**"
                                     "/=" "/==" "/>" "//" "///" "&&" "||" "||=" "|=" "|>" "^=" "$>"
                                     "++" "+++" "+>" "=:=" "==" "===" "==>" "=>" "=>>" "<="
                                     "=<<" "=/=" ">-" ">=" ">=>" ">>" ">>-" ">>=" ">>>" "<*"
                                     "<*>" "<|" "<|>" "<$" "<$>" "<!--" "<-" "<--" "<->" "<+"
                                     "<+>" "<=" "<==" "<=>" "<=<" "<>" "<<" "<<-" "<<=" "<<<"
                                     "<~" "<~~" "</" "</>" "~@" "~-" "~>" "~~" "~~>" "%%"))

(global-ligature-mode 't)

init-org.el

org-roam 和 Emacs 自带的 agenda 可以很方便的将笔记和 GTD 结合起来。

;;; init-org.el --- Org-mode config -*- lexical-binding: t -*-
;;; Commentary:

;; Among settings for many aspects of `org-mode', this code includes
;; an opinionated setup for the Getting Things Done (GTD) system based
;; around the Org Agenda.  I have an "inbox.org" file with a header
;; including

;;     #+CATEGORY: Inbox
;;     #+FILETAGS: INBOX

;; and then set this file as `org-default-notes-file'.  Captured org
;; items will then go into this file with the file-level tag, and can
;; be refiled to other locations as necessary.

;; Those other locations are generally other org files, which should
;; be added to `org-agenda-files-list' (along with "inbox.org" org).
;; With that done, there's then an agenda view, accessible via the
;; `org-agenda' command, which gives a convenient overview.
;; `org-todo-keywords' is customised here to provide corresponding
;; TODO states, which should make sense to GTD adherents.

;;; Code:
;; 都是方便插入超链接的
(when *IS-MAC*
  (maybe-require-package 'grab-mac-link))
(maybe-require-package 'org-cliplink)
;; 图片宽度
(setq org-image-actual-width nil)
(add-hook 'org-mode-hook 'org-indent-mode)
;; 去除代码块内的缩进
(setq org-edit-src-content-indentation 0)
(setq org-src-preserve-indentation t)
(define-key global-map (kbd "C-c l") 'org-store-link)
(define-key global-map (kbd "C-c a") 'org-agenda)

(defvar sanityinc/org-global-prefix-map (make-sparse-keymap)
  "A keymap for handy global access to org helpers, particularly clocking.")

(define-key sanityinc/org-global-prefix-map (kbd "j") 'org-clock-goto)
(define-key sanityinc/org-global-prefix-map (kbd "l") 'org-clock-in-last)
(define-key sanityinc/org-global-prefix-map (kbd "i") 'org-clock-in)
(define-key sanityinc/org-global-prefix-map (kbd "o") 'org-clock-out)
(define-key global-map (kbd "C-c o") sanityinc/org-global-prefix-map)


;; Various preferences
(setq org-log-done t
      org-edit-timestamp-down-means-later t
      org-hide-emphasis-markers t
      org-catch-invisible-edits 'show
      org-export-coding-system 'utf-8
      org-fast-tag-selection-single-key 'expert
      org-html-validation-link nil
      org-export-kill-product-buffer-when-displayed t
      org-tags-column 80)


;; Lots of stuff from http://doc.norang.ca/org-mode.html

;; Re-align tags when window shape changes
(with-eval-after-load 'org-agenda
  (add-hook 'org-agenda-mode-hook
            (lambda () (add-hook 'window-configuration-change-hook 'org-agenda-align-tags nil t))))

;;; Capturing
(setq org-directory "~/Dropbox/org/")
(global-set-key (kbd "C-c c") 'org-capture)
(setq org-capture-templates
      `(("i" "inbox" entry  (file "agenda/inbox.org")
         ,(concat "* TODO %?\n%U"))
        ("n" "note" entry (file "agenda/note.org")
         "* %? :NOTE:\n%U\n%a\n" :clock-resume t)
        ))



;;; Refiling

(setq org-refile-use-cache nil)

;; Targets include this file and any file contributing to the agenda - up to 5 levels deep
(setq org-refile-targets '((nil :maxlevel . 5) (org-agenda-files :maxlevel . 5)))

(with-eval-after-load 'org-agenda
  (add-to-list 'org-agenda-after-show-hook 'org-show-entry))

(advice-add 'org-refile :after (lambda (&rest _) (org-save-all-org-buffers)))

;; Exclude DONE state tasks from refile targets
(defun sanityinc/verify-refile-target ()
  "Exclude todo keywords with a done state from refile targets."
  (not (member (nth 2 (org-heading-components)) org-done-keywords)))
(setq org-refile-target-verify-function 'sanityinc/verify-refile-target)

(defun sanityinc/org-refile-anywhere (&optional goto default-buffer rfloc msg)
  "A version of `org-refile' which allows refiling to any subtree."
  (interactive "P")
  (let ((org-refile-target-verify-function))
    (org-refile goto default-buffer rfloc msg)))

(defun sanityinc/org-agenda-refile-anywhere (&optional goto rfloc no-update)
  "A version of `org-agenda-refile' which allows refiling to any subtree."
  (interactive "P")
  (let ((org-refile-target-verify-function))
    (org-agenda-refile goto rfloc no-update)))

;; Targets start with the file name - allows creating level 1 tasks
;;(setq org-refile-use-outline-path (quote file))
(setq org-refile-use-outline-path t)
(setq org-outline-path-complete-in-steps nil)

;; Allow refile to create parent tasks with confirmation
(setq org-refile-allow-creating-parent-nodes 'confirm)


;;; To-do settings
;; TODO
;; HOLD(h@)       ; 进入时添加笔记
;; HOLD(h/!)      ; 离开时添加变更信息
;; HOLD(h@/!)     ; 进入时添加笔记,离开时添加变更信息
(setq org-todo-keywords
      (quote ((sequence "TODO(t)" "NEXT(n)" "|" "DONE(d!/!)")
              (sequence "PROJECT(p)" "|" "DONE(d!/!)" "CANCELLED(c/!)")
              (sequence "WAITING(w/!)" "DELEGATED(e!)" "HOLD(h)" "|" "CANCELLED(c/!)")))
      org-todo-repeat-to-state "NEXT")

(setq org-todo-keyword-faces
      (quote (("NEXT" :inherit warning)
              ("PROJECT" :inherit font-lock-string-face))))



;;; Agenda views
;; 将没有时间标记的任务,放在上方显示。
(setq org-agenda-sort-notime-is-late nil)
;; 时间显示为两位数(9:30 -> 09:30)
(setq org-agenda-time-leading-zero t)
;; 过滤掉部分 tags
(setq org-agenda-hide-tags-regexp (regexp-opt '("dynamic")))
(setq org-agenda-files (file-expand-wildcards "~/Dropbox/org/agenda/*.org"))
(setq-default org-agenda-clockreport-parameter-plist '(:link t :maxlevel 3))

;; 计算待办事项创建至今的时间
(defun org-todo-age (&optional pos)
    (if-let* ((entry-age (org-todo-age-time pos))
              (days (time-to-number-of-days entry-age)))
        (cond
         ((< days 1)   "today")
         ((< days 7)   (format "%dd" days))
         ((< days 30)  (format "%.1fw" (/ days 7.0)))
         ((< days 358) (format "%.1fM" (/ days 30.0)))
         (t            (format "%.1fY" (/ days 365.0))))
      ""))

(defun org-todo-age-time (&optional pos)
  (let ((stamp (org-entry-get (or pos (point)) "TIMESTAMP_IA" t)))
    (when stamp
      (time-subtract (current-time)
                     (org-time-string-to-time stamp)))))

  (setq org-agenda-compact-blocks t
        org-agenda-sticky t
        org-agenda-start-on-weekday nil
        org-agenda-span 'day
        org-agenda-include-diary nil
        org-agenda-sorting-strategy
        '((agenda habit-down time-up user-defined-up effort-up category-keep)
          (todo category-up effort-up)
          (tags category-up effort-up)
          (search category-up))
        org-agenda-window-setup 'current-window
        org-agenda-custom-commands
        `(("N" "Notes" tags "NOTE"
           ((org-agenda-overriding-header "Notes")
            (org-tags-match-list-sublevels t)))
          ("g" "GTD"
           ((agenda "" nil)
            (tags-todo "-inbox"
                       ((org-agenda-overriding-header "Next Actions")
                        (org-agenda-tags-todo-honor-ignore-options t)
                        (org-agenda-todo-ignore-scheduled 'future)
                        (org-agenda-skip-function
                         '(lambda ()
                            (or (org-agenda-skip-subtree-if 'todo '("HOLD" "WAITING"))
                                (org-agenda-skip-entry-if 'nottodo '("NEXT")))))
                        (org-tags-match-list-sublevels t)
                        (org-agenda-sorting-strategy
                         '(todo-state-down effort-up category-keep))))
            (tags-todo "-reading&-book/PROJECT"
                       ((org-agenda-overriding-header "Project")
                       (org-agenda-prefix-format "%-11c%5(org-todo-age) ")
                        (org-tags-match-list-sublevels t)
                        (org-agenda-sorting-strategy
                         '(category-keep))))
            (tags-todo "+reading&+book/PROJECT"
                       ((org-agenda-overriding-header "Reading")
                       (org-agenda-prefix-format "%-11c%5(org-todo-age) ")
                        (org-tags-match-list-sublevels t)
                        (org-agenda-sorting-strategy
                         '(category-keep))))
            (tags-todo "/WAITING"
                       ((org-agenda-overriding-header "Waiting")
                        (org-agenda-tags-todo-honor-ignore-options t)
                        (org-agenda-todo-ignore-scheduled 'future)
                        (org-agenda-sorting-strategy
                         '(category-keep))))
            (tags-todo "/DELEGATED"
                       ((org-agenda-overriding-header "Delegated")
                        (org-agenda-tags-todo-honor-ignore-options t)
                        (org-agenda-todo-ignore-scheduled 'future)
                        (org-agenda-sorting-strategy
                         '(category-keep))))
            (tags-todo "-inbox"
                       ((org-agenda-overriding-header "On Hold")
                        (org-agenda-skip-function
                         '(lambda ()
                            (or (org-agenda-skip-subtree-if 'todo '("WAITING"))
                                (org-agenda-skip-entry-if 'nottodo '("HOLD")))))
                        (org-tags-match-list-sublevels nil)
                        (org-agenda-sorting-strategy
                         '(category-keep))))
            ))
          ("v" "Orphaned Tasks"
           ((agenda "" nil)
            (tags "inbox"
                  ((org-agenda-overriding-header "Inbox")
                  (org-agenda-prefix-format "%-11c%5(org-todo-age) ")
                   (org-tags-match-list-sublevels nil)))
           (tags-todo "+book&-reading/PROJECT"
                       ((org-agenda-overriding-header "Book Plan")
                       (org-agenda-prefix-format "%-11c%5(org-todo-age) ")
                        (org-tags-match-list-sublevels t)
                        (org-agenda-sorting-strategy
                         '(category-keep))))
            (tags-todo "-inbox/-NEXT"
                       ((org-agenda-overriding-header "Orphaned Tasks")
                        (org-agenda-tags-todo-honor-ignore-options t)
                        (org-agenda-prefix-format "%-11c%5(org-todo-age) ")
                        (org-agenda-todo-ignore-scheduled 'future)
                        (org-agenda-skip-function
                         '(lambda ()
                            (or (org-agenda-skip-subtree-if 'todo '("PROJECT" "HOLD" "WAITING" "DELEGATED"))
                                (org-agenda-skip-subtree-if 'nottododo '("TODO")))))
                        (org-tags-match-list-sublevels t)
                        (org-agenda-sorting-strategy
                         '(category-keep))))
            ))))


(add-hook 'org-agenda-mode-hook 'hl-line-mode)
(advice-add 'org-refile :after 'org-save-all-org-buffers)
(advice-add 'org-archive :after 'org-save-all-org-buffers)
(add-hook 'org-capture-after-finalize-hook 'org-save-all-org-buffers)
(advice-add 'org-capture-refile :after 'org-save-all-org-buffers)


;;; Org clock

;; Save the running clock and all clock history when exiting Emacs, load it on startup
(with-eval-after-load 'org
  (org-clock-persistence-insinuate))
(setq org-clock-persist t)
(setq org-clock-in-resume t)

;; Save clock data and notes in the LOGBOOK drawer
(setq org-clock-into-drawer t)
;; Save state changes in the LOGBOOK drawer
(setq org-log-into-drawer t)
;; Removes clocked tasks with 0:00 duration
(setq org-clock-out-remove-zero-time-clocks t)

;; Show clock sums as hours and minutes, not "n days" etc.
(setq org-time-clocksum-format
      '(:hours "%d" :require-hours t :minutes ":%02d" :require-minutes t))


;;; Show the clocked-in task - if any - in the header line
(defun sanityinc/show-org-clock-in-header-line ()
  (setq-default header-line-format '((" " org-mode-line-string " "))))

(defun sanityinc/hide-org-clock-from-header-line ()
  (setq-default header-line-format nil))

(add-hook 'org-clock-in-hook 'sanityinc/show-org-clock-in-header-line)
(add-hook 'org-clock-out-hook 'sanityinc/hide-org-clock-from-header-line)
(add-hook 'org-clock-cancel-hook 'sanityinc/hide-org-clock-from-header-line)

(with-eval-after-load 'org-clock
  (define-key org-clock-mode-line-map [header-line mouse-2] 'org-clock-goto)
  (define-key org-clock-mode-line-map [header-line mouse-1] 'org-clock-menu))


;;; Archiving

(setq org-archive-mark-done nil)
(setq org-archive-location "%s_archive::* Archive")

(with-eval-after-load 'org
  (define-key org-mode-map (kbd "C-M-<up>") 'org-up-element)
  (when *IS-MAC*
    (define-key org-mode-map (kbd "M-h") nil)
    (define-key org-mode-map (kbd "C-c g") 'grab-mac-link)))

(with-eval-after-load 'org
  (org-babel-do-load-languages
   'org-babel-load-languages
   (seq-filter
    (lambda (pair)
      (locate-library (concat "ob-" (symbol-name (car pair)))))
    '((R . t)
      (ditaa . t)
      (rust . t)
      (dot . t)
      (emacs-lisp . t)
      (gnuplot . t)
      (haskell . nil)
      (ledger . t)
      (ocaml . nil)
      (octave . t)
      (ruby . t)
      (screen . nil)
      (sh . t) ;; obsolete
      (shell . t)
      (sql . t)
      (sqlite . t))))
  ;; plantuml
  (org-babel-do-load-languages
   'org-babel-load-languages
   '(;; other Babel languages
     (plantuml . t)
     (python . t)
     (latex . t)))
  (setq  org-plantuml-jar-path
         (expand-file-name "~/Dropbox/org/plantuml/plantuml.jar")))
;; 这里应该就是 .zshrc 里面配置的 python3
(setq org-babel-python-command "python3")

(require-package 'emacsql-sqlite-builtin)
(require 'emacsql-sqlite-builtin)
(use-package org-roam
  :ensure t
  :demand t ;; ensure org-roam is loaded by default
  :init
  :custom
  (org-roam-directory (file-truename "~/Dropbox/org/"))
  ;; 需要 emacs-plus@29 版本
  (org-roam-database-connector 'sqlite-builtin)
  (org-roam-db-location "~/Dropbox/org/org.db")
  (org-roam-db-gc-threshold most-positive-fixnum)
  (org-roam-completion-everywhere t)
  (org-roam-capture-templates
   '(
     ;; #+OPTIONS: toc:nil 为了导出 .md 的格式更加符合使用
     ("d" "default" plain
      (file "~/Dropbox/org/templates/default.org")
      :if-new (file "main/%<%Y%m%d%H%M%S>-${slug}.org")
      :unnarrowed t)
     ("p" "private" plain
      (file "~/Dropbox/org/templates/private.org")
      :if-new (file "private/%<%Y%m%d%H%M%S>-${slug}.org")
      :unnarrowed t)
     ("a" "article" plain
      (file "~/Dropbox/org/templates/article.org")
      :if-new (file "main/%<%Y%m%d%H%M%S>-${slug}.org")
      :unnarrowed t)
     ("n" "article-network" plain
      (file "~/Dropbox/org/templates/article-network.org")
      :if-new (file "main/%<%Y%m%d%H%M%S>-${slug}.org")
      :unnarrowed t)
     )
   )
  (org-roam-dailies-capture-templates
   ;; %<%H:%M> 为24小时制,%<%I:%M %p> 为12小时制
   '(
     ("d" "default" entry "** %<%H:%M> %?"
      :if-new (file+head+olp "%<%Y-%m-%d>.org" "#+title: %<%Y-%m-%d>\n#+ARCHIVE: journal.org::\n" ("%<%Y-%m-%d>")))
     ("r" "read" entry "*** %?"
      :if-new (file+head+olp "%<%Y-%m-%d>.org" "#+title: %<%Y-%m-%d>\n#+ARCHIVE: journal.org::\n" ("%<%Y-%m-%d>" "What I read? :read:")))
     ("t" "tasks" entry "*** %?"
      :if-new (file+head+olp "%<%Y-%m-%d>.org" "#+title: %<%Y-%m-%d>\n#+ARCHIVE: journal.org::\n" ("%<%Y-%m-%d>" "Tasks :task:")))
     ("n" "notes" entry "*** %?"
      :if-new (file+head+olp "%<%Y-%m-%d>.org" "#+title: %<%Y-%m-%d>\n#+ARCHIVE: journal.org::\n" ("%<%Y-%m-%d>" "Notes :note:")))
     ("o" "online" entry "** %<%H:%M> %? :online:"
      :if-new (file+head+olp "%<%Y-%m-%d>.org" "#+title: %<%Y-%m-%d>\n#+ARCHIVE: journal.org::\n" ("%<%Y-%m-%d>")))
     )
   )
  :bind (("C-c n l" . org-roam-buffer-toggle)
         ("C-c n f" . org-roam-node-find)
         ("C-c n i" . org-roam-node-insert)
         ("C-c n j" . org-roam-dailies-capture-today)
         ("C-c n I" . org-roam-node-insert-immediate)
         ("C-c n m" . dired-copy-images-links)
         :map org-mode-map
         ;; Ctrl-Alt-i
         ("C-M-i" . completion-at-point))
  :config
  (unless (file-exists-p org-roam-directory)
    (make-directory org-roam-directory t))
  (org-roam-db-autosync-enable)
  ;;使用侧边栏而不是完整buffer
  (add-to-list 'display-buffer-alist
               '("\\*org-roam\\*"
                 (display-buffer-in-direction)
                 (direction . right)
                 (window-width . 0.33)
                 (window-height . fit-window-to-buffer)))
  (setq org-export-backends (quote (ascii html icalendar latex md)))
  ;; code hightlight
  (setq org-src-fontify-natively t)
  ;; If using org-roam-protocol
  (require 'org-roam-protocol)
  (require 'org-roam-export)
  (require 'org-tempo)
  ;; Copy Done To-Dos to Today
  (defun org-roam-copy-todo-to-today ()
    (interactive)
    (let ((org-refile-keep t) ;; Set this to nil to delete the original!
          (org-after-refile-insert-hook #'save-buffer)
          today-file
          pos)
      (save-window-excursion
        (org-roam-dailies-capture-today t "t")
        (setq today-file (buffer-file-name))
        (setq pos (point)))

      ;; Only refile if the target file is different than the current file
      (unless (equal (file-truename today-file)
                     (file-truename (buffer-file-name)))
        (org-refile nil nil (list "Tasks" today-file nil pos)))))
  (add-to-list 'org-after-todo-state-change-hook
               (lambda ()
                 ;; DONE 和 CANCELLED 的 To-Dos 自动复制到今日
                 ;; 同时过滤掉 habit 的 To-Dos
                 (when (and (or (equal org-state "DONE") (equal org-state "CANCELLED")) (not (org-find-property "STYLE")))
                   (org-roam-copy-todo-to-today))))
  ;; node-find 的时候展示文件夹
  ;; org-roam-node-type
  (cl-defmethod org-roam-node-type ((node org-roam-node))
    "Return the TYPE of NODE."
    (condition-case nil
        (file-name-nondirectory
         (directory-file-name
          (file-name-directory
           (file-relative-name (org-roam-node-file node) org-roam-directory))))
      (error "")))
  ;; I encountered the following message when attempting
  ;; to export data:
  ;;
  ;; "org-export-data: Unable to resolve link: FILE-ID"
  (defun force-org-rebuild-cache ()
    "Rebuild the `org-mode' and `org-roam' cache."
    (interactive)
    (org-id-update-id-locations)
    ;; Note: you may need `org-roam-db-clear-all'
    ;; followed by `org-roam-db-sync'
    (org-roam-db-sync)
    (org-roam-update-org-id-locations))
  ;; org-roam 作者提供的解决办法
  ;; (setq org-id-track-globally t)
  ;; M-x org-id-update-id-locations
  )
;; 解决 org-roam-node-find 时,内容局限于 buffer 宽度。
;; https://github.com/org-roam/org-roam/issues/2066
(defun my/org-roam-node-read--to-candidate (node template)
  "Return a minibuffer completion candidate given NODE.
TEMPLATE is the processed template used to format the entry."
  (let ((candidate-main (org-roam-node--format-entry
                         template
                         node
                         (1- (if (minibufferp)
                                 (window-width) (frame-width))))))
    (cons (propertize candidate-main 'node node) node)))

(advice-add 'org-roam-node-read--to-candidate :override #'my/org-roam-node-read--to-candidate)
;; 在记录的时候创建新的 node 时不退出当前状态,保存新建的 node。
(defun org-roam-node-insert-immediate (arg &rest args)
  (interactive "P")
  (let ((args (push arg args))
        (org-roam-capture-templates (list (append (car org-roam-capture-templates)
                                                  '(:immediate-finish t)))))
    (apply #'org-roam-node-insert args)))

;; C-x d 进入 dired 模式,m 来标记对应需要复制链接的图片,C-c n m 即可复制到需要的图片插入文本。
;; source: https://org-roam.discourse.group/t/is-there-a-solution-for-images-organization-in-org-roam/925
(defun dired-copy-images-links ()
  "Works only in dired-mode, put in kill-ring,
  ready to be yanked in some other org-mode file,
  the links of marked image files using file-name-base as #+CAPTION.
  If no file marked then do it on all images files of directory.
  No file is moved nor copied anywhere.
  This is intended to be used with org-redisplay-inline-images."
  (interactive)
  (if (derived-mode-p 'dired-mode)                           ; if we are in dired-mode
      (let* ((marked-files (dired-get-marked-files))         ; get marked file list
             (number-marked-files                            ; store number of marked files
              (string-to-number                              ; as a number
               (dired-number-of-marked-files))))             ; for later reference
        (when (= number-marked-files 0)                      ; if none marked then
          (dired-toggle-marks)                               ; mark all files
          (setq marked-files (dired-get-marked-files)))      ; get marked file list
        (message "Files marked for copy")                    ; info message
        (dired-number-of-marked-files)                       ; marked files info
        (kill-new "\n")                                      ; start with a newline
        (dolist (marked-file marked-files)                   ; walk the marked files list
          (when (org-file-image-p marked-file)               ; only on image files
            (kill-append                                     ; append image to kill-ring
             (concat "#+CAPTION: "                           ; as caption,
                     (file-name-base marked-file)            ; use file-name-base
                     "\n#+ATTR_ORG: :width 800"              ; img width
                     "\n[[file:" marked-file "]]\n\n") nil))); link to marked-file
        (when (= number-marked-files 0)                      ; if none were marked then
          (dired-toggle-marks)))                             ; unmark all
    (message "Error: Does not work outside dired-mode")      ; can't work not in dired-mode
    (ding)))                                                 ; error sound

;; Save the corresponding buffers
(defun gtd-save-org-buffers ()
  "Save `org-agenda-files' buffers without user confirmation.
See also `org-save-all-org-buffers'"
  (interactive)
  (message "Saving org-agenda-files buffers...")
  (save-some-buffers t (lambda ()
             (when (member (buffer-file-name) org-agenda-files)
               t)))
  (message "Saving org-agenda-files buffers... done"))
;; Add it after refile
(advice-add 'org-refile :after (lambda (&rest _) (gtd-save-org-buffers)))

(defun log-todo-next-creation-date (&rest ignore)
  "Log NEXT creation time in the property drawer under the key 'ACTIVATED'"
  (when (and (string= (org-get-todo-state) "NEXT")
             (not (org-entry-get nil "ACTIVATED")))
    (org-entry-put nil "ACTIVATED" (format-time-string "[%Y-%m-%d]"))))
(add-hook 'org-after-todo-state-change-hook #'log-todo-next-creation-date)

;; Put the text property afterwards with an advice
(defun my-org-agenda-override-header (orig-fun &rest args)
  "Change the face of the overriden header string if needed.
The propertized header text is taken from `org-agenda-overriding-header'.
The face is only changed if the overriding header is propertized with a face."
  (let ((pt (point))
        (header org-agenda-overriding-header))
    (apply orig-fun args)
    ;; Only replace if there is an overriding header and not an empty string.
    ;; And only if the header text has a face property.
    (when (and header (> (length header) 0)
               (get-text-property 0 'face header))
      (save-excursion
        (goto-char pt)
        ;; Search for the header text.
        (search-forward header)
        (unwind-protect
            (progn
              (read-only-mode -1)
              ;; Replace it with the propertized text.
              (replace-match header))
          (read-only-mode 1))))))

(defun my-org-agenda-override-header-add-advices ()
  "Add advices to make changing work in all agenda commands."
  (interactive)
  (dolist (fun '(org-agenda-list org-todo-list org-search-view org-tags-view))
    (advice-add fun :around #'my-org-agenda-override-header)))
(my-org-agenda-override-header-add-advices)
;; 只显示 outline 下第一个 TODO
(defun my-org-agenda-skip-all-siblings-but-first ()
  "Skip all but the first non-done entry."
  (let (should-skip-entry)
    (unless (org-current-is-todo)
      (setq should-skip-entry t))
    (save-excursion
      (while (and (not should-skip-entry) (org-goto-sibling t))
        (when (org-current-is-todo)
          (setq should-skip-entry t))))
    (when should-skip-entry
      (or (outline-next-heading)
          (goto-char (point-max))))))

(defun org-current-is-todo ()
  (string= "TODO" (org-get-todo-state)))
;; 导出特定文件夹下所有内容到 hugo
(defun ox-hugo/export-all (&optional org-files-root-dir dont-recurse)
  "Export all Org files (including nested) under ORG-FILES-ROOT-DIR.

  All valid post subtrees in all Org files are exported using
  `org-hugo-export-wim-to-md'.

  If optional arg ORG-FILES-ROOT-DIR is nil, all Org files in
  current buffer's directory are exported.

  If optional arg DONT-RECURSE is nil, all Org files in
  ORG-FILES-ROOT-DIR in all subdirectories are exported. Else, only
  the Org files directly present in the current directory are
  exported.  If this function is called interactively with
  \\[universal-argument] prefix, DONT-RECURSE is set to non-nil.

  Example usage in Emacs Lisp: (ox-hugo/export-all \"~/org\")."
  (interactive)
  (org-transclusion-mode 1)
  (let* ((org-files-root-dir (or org-files-root-dir default-directory))
         (dont-recurse (or dont-recurse (and current-prefix-arg t)))
         (search-path (file-name-as-directory (expand-file-name org-files-root-dir)))
         (org-files (if dont-recurse
                        (directory-files search-path :full "\.org$")
                      (directory-files-recursively search-path "\.org$")))
         (num-files (length org-files))
         (cnt 1))
    (if (= 0 num-files)
        (message (format "No Org files found in %s" search-path))
      (progn
        (message (format (if dont-recurse
                             "[ox-hugo/export-all] Exporting %d files from %S .."
                           "[ox-hugo/export-all] Exporting %d files recursively from %S ..")
                         num-files search-path))
        (dolist (org-file org-files)
          (with-current-buffer (find-file-noselect org-file)
            (message (format "[ox-hugo/export-all file %d/%d] Exporting %s" cnt num-files org-file))
            (org-hugo-export-wim-to-md :all-subtrees)
            (setq cnt (1+ cnt))))
        (org-transclusion-mode -1)
        (message "Done!")))))
(provide 'init-org)
;;; init-org.el ends here

Org-babel

输出 :results 有不同的选项,其中 silent 是指:Do not insert results in the Org mode buffer, but echo them in the minibuffer. Usage example: ‘:results output silent’.

Rust 运行代码块,需要安装 GitHub - brotzeit/rustic: Rust development environment for Emacs,具体执行的时候会提示需要安装 lsp-mode​,确定即可。

PlantUML

brew install graphviz
Type Symbol
Zero or One o|–
Exactly One ||–
Zero or Many }o–
One or Many }|–
@startumlg
scale 2
Entity01 }|..|| Entity02
Entity03 }o..o| Entity04
Entity05 ||--o{ Entity06
Entity07 |o--|| Entity08
@enduml

PlantUML 默认的样式比较古老,可以用更加现代一些的样式:GitHub - xuanye/plantuml-style-c4: 自定义的plantuml 样式,不过好久没更新了,前面的颜色也是参考的这个:GitHub - plantuml-stdlib/C4-PlantUML: C4-PlantUML combines the benefits of Pl…

init-sis.el

根据 meow 的 insert mode 切换,同时根据上下文来判断切换输入法;另外在特定 mode 下也切换输入法,使用更加顺滑。

需要安装 macism

brew tap laishulu/macism
brew install macism
;;; init-sis.el  --- Custom configuration
;;; Commentary
(require-package 'sis)
;; meow insert mode switch
(defvar meow-leaving-insert-mode-hook nil
    "Hook to run when leaving meow insert mode.")
(defvar meow-entering-insert-mode-hook nil
    "Hook to run when entering meow insert mode.")
(add-hook 'meow-insert-mode-hook
    (lambda ()
        (if meow-insert-mode
            (run-hooks 'meow-entering-insert-mode-hook)
          (run-hooks 'meow-leaving-insert-mode-hook))))
(use-package sis
  :config
  (setq sis-english-source "com.apple.keylayout.ABC")
  (if *IS-MAC*
      (sis-ism-lazyman-config
       "com.apple.keylayout.ABC"
       "im.rime.inputmethod.Squirrel.Hans" 'macism)
    (sis-ism-lazyman-config "1" "2" 'fcitx))
  ;; enable the /cursor color/ mode
  (sis-global-cursor-color-mode t)
  ;; enable the /respect/ mode
  ;; 打开会影响 meow-reverse,因为 meow 下退出模式编辑后都是默认英文,这里也无需开启。
  ;; (sis-global-respect-mode t)
  ;; enable the /context/ mode for all buffers
  (sis-global-context-mode t)
  ;; enable the /inline english/ mode for all buffers
  ;; (sis-global-inline-mode t)
  ;; not delete the head spaces
  (setq sis-inline-tighten-head-rule nil)
  (setq sis-default-cursor-color "#FE6DB3")
  (setq sis-other-cursor-color "orange")
  (setq sis-prefix-override-keys (list "C-c" "C-x" "C-h"))

  (add-hook 'meow-leaving-insert-mode-hook #'sis-set-english)
  (add-to-list 'sis-context-hooks 'meow-entering-insert-mode-hook)
  ;; org title 处切换 Rime,telega 聊天时切换 Rime。
  (add-to-list 'sis-context-detectors
               (lambda (&rest _)
                 ;; (when (or (and (eq major-mode 'org-mode) (org-at-heading-p))
                 (when (or (eq major-mode 'org-mode)
                           (eq major-mode 'telega-chat-mode))
                   'other)))
  (advice-add 'org-agenda-todo :before #'sis-set-english)
  (advice-add 'hydra-org-agenda-menu/body :after #'sis-set-english)
  :ensure)
;; MacOS 上防止进入 insert mode 覆盖要切换的中文状态
(defun sis--respect-focus-in-handler ())
(provide 'init-sis)
;;; init-sis.el ends here

注:从其他 App 切回 Emacs 后进入 insert mode 之后切换到中文后立刻切换到英文,解决办法如下。(问题 emacs -Q 记录视频 Imgur: The magic of the Internet

是这样的, 你是从其它应用切回 emacs 后, 立刻就 i 进入 insert mode?

如果稍微延迟一下再 i 进入 insert mode, 是不是就没这个问题了?

emacs 处理窗口焦点的切换(focus in事件)是有一个延迟的, 这样操作就导致:

focus in 之后恢复当前 buffer 的 input source(因为离开前为 normal 状态,所以为 english) 你进入 insert mode 切换 input source(为 rime) 这二者抢占了。

比较 dirty 但是直接的办法是, 你把下面这个函数在自己的配置文件中重新定义一下,让 sis 不要切换。

emacs-smart-input-source/sis.el at 02777a46a4b7c18c2cd588e6e2f11e83ba2378e4 ·…

但是这块也不算是 sis 的 bug, sis 对于焦点切换也只能做到这一步。 包治百病的用法就是切回emacs稍微等一下。

不过我又想到一点, macos 可以系统配置“自动切换到文稿的输入法”, 你如果配置了这个, 在使用 GUI Emacs 时,应该是不需要 sis 自己去恢复当前 buffer 的输入法的。 那就可以用空函数去覆盖上面那个函数了。 但是!!! macos 也会帮你主动地切输入法,就是不知道会不会也有延迟,导致同样的问题

不过 sis 针对的是一般情况:非 MacOS 系统,或没系统设置“自动切换到文稿的输入法”,或用的是 Terminal Emacs 等等。

init-font.el

使用的是 Iosevka 和大多数中文字体配合,按照 1:1 缩放,偶数字号就可以做到等高等宽。这是选择 LXGW WenKai Screen 而不是 LXGW WenKai Mono 是因为后者的粗体非常不明显,作为文档文字不好区分。

;;; init-font.el --- Dired customisations -*- lexical-binding: t -*-
;;; Commentary:
;;; Code:
;;; base on https://gist.github.com/coldnew/7398835
(defun qiang-font-existsp (font)
  (if (null (x-list-fonts font))
      nil
    t))
;; LXGW WenKai Mono 配合 Iosevka 按照 1:1 缩放,偶数字号就可以做到等高等宽。
(defvar zh-font-list '("LXGW WenKai Screen" "FZSongKeBenXiuKai-R-GBK" "HanaMinB"))
(defvar en-font-list '("Iosevka" "Latin Modern Mono" "Fira Code" "IBM Plex Mono"))

(defun qiang-make-font-string (font-name font-size)
  (if (and (stringp font-size)
           (equal ":" (string (elt font-size 0))))
      (format "%s%s" font-name font-size)
    (format "%s %s" font-name font-size)))

(defun qiang-set-font (english-fonts
                       english-font-size
                       chinese-fonts
                       &optional chinese-font-scale)

  (setq chinese-font-scale (or chinese-font-scale 1))

  (setq face-font-rescale-alist
        (cl-loop for x in zh-font-list
              collect (cons x chinese-font-scale)))

  "english-font-size could be set to \":pixelsize=18\" or a integer.
If set/leave chinese-font-scale to nil, it will follow english-font-size"

  (let ((en-font (qiang-make-font-string
                  (cl-find-if #'qiang-font-existsp english-fonts)
                  english-font-size))
        (zh-font (font-spec :family (cl-find-if #'qiang-font-existsp chinese-fonts))))

    ;; Set the default English font
    (message "Set English Font to %s" en-font)
    (set-face-attribute 'default nil :font en-font)

    ;; Set Chinese font
    ;; Do not use 'unicode charset, it will cause the English font setting invalid
    (message "Set Chinese Font to %s" zh-font)
    (dolist (charset '(kana han symbol cjk-misc bopomofo))
      (set-fontset-font (frame-parameter nil 'font)
                        charset zh-font))))

(qiang-set-font en-font-list 14 zh-font-list)
(add-to-list 'face-font-rescale-alist '("Apple Color Emoji" . 0.8))
(provide 'init-font)
;;; init-font.el ends here

init-rime.el

需要安装 Squirrel 并下载与之对应版本的 librime。

curl -L -O https://github.com/rime/librime/releases/download/1.7.3/rime-1.7.3-osx.zip
unzip rime-1.7.3-osx.zip -d ~/.emacs.d/librime
rm -rf rime-1.7.3-osx.zip

使用拼音输入法需要维护两份配置,但是备份位置可以是同一个地址,这样就达到了同步配置的目的。

cp -rf ~/Library/Rime ~/.emacs.d/
;;; init-rime.el  --- Custom configuration
;;; Commentary
(require-package 'rime)
(use-package rime
  :custom
  (default-input-method "rime")
  (rime-librime-root "~/.emacs.d/librime/dist")
  (rime-emacs-module-header-root "/usr/local/Cellar/emacs-plus@29/29.0.50/include")
  :config
  (define-key rime-mode-map (kbd "C-i") 'rime-force-enable)
  ;; 方案切换选择
  (define-key rime-mode-map (kbd "C-`") 'rime-send-keybinding)
  (setq rime-disable-predicates
        '(meow-normal-mode-p
                                  meow-motion-mode-p
                                  meow-keypad-mode-p
          ;; If cursor is in code.
          rime-predicate-prog-in-code-p
          ;; If the cursor is after a alphabet character.
          rime-predicate-after-alphabet-char-p
          ;; If input a punctuation after
          ;; a Chinese charactor with whitespace.
          rime-predicate-punctuation-after-space-cc-p
          rime-predicate-special-ascii-line-begin-p))
  (setq rime-inline-predicates
        ;; If cursor is after a whitespace
        ;; which follow a non-ascii character.
        '(rime-predicate-space-after-cc-p
          ;; If the current charactor entered is a uppercase letter.
          rime-predicate-current-uppercase-letter-p))
  ;;; support shift-l, shift-r, control-l, control-r
  (setq rime-inline-ascii-trigger 'shift-r)
  ;; meow 进入 insert-mode,且是 org-mode 或
  ;; telega-chat-mode 时,切换到 Rime。
  (add-hook 'meow-insert-enter-hook
            (lambda() (when (derived-mode-p 'org-mode 'telega-chat-mode)
                        (set-input-method "rime"))))
  ;; 退出 insert mode 时,恢复英文。
  (add-hook 'meow-insert-exit-hook
            (lambda() (set-input-method nil)))
  (setq rime-user-data-dir "~/.emacs.d/Rime"))

(defun rime-predicate-special-ascii-line-begin-p ()
  "If '/' or '#' at the beginning of the line."
  (and (> (point) (save-excursion (back-to-indentation) (point)))
       (let ((string (buffer-substring (point) (max (line-beginning-position) (- (point) 80)))))
         (string-match-p "^[\/#]" string))))
(provide 'init-rime)
;;; init-rime.el ends here

其中 (setq rime-inline-ascii-trigger 'shift-r) 需要配合 Rime 中 default.custom.yaml 配置中的 Shift_R: inline_ascii 才可生效。

patch:
# commit_code 上屏输出 code 并切换成英文
# commit_text 上屏输出内容并切换成英文
# clear 清除内容并切换成英文
  ascii_composer/good_old_caps_lock: true
  ascii_composer/switch_key:
    Caps_Lock: commit_code
    Control_L: noop
    Control_R: noop
    Shift_L: commit_code
    Shift_R: inline_ascii

最终实现的效果是。

  • 进入 meow 的 insert-mode 自动启用 Rime,退出 insert-mode 后关闭 Rime。
  • 在编程界面,Rime 开启时,代码中自动切换英文,注释中自动切换中文。
  • 字母及英文字符紧跟后面输入自动切换英文。(遇到以英文在半句或者整句后的情况下,英文后要输入逗号或者句号等中文符号,可以使用 rime-force-enable 来强制中文模式,这样就可以在前面描述的情况下强制输入中文符号。)
  • #/ 开头,则切换英文。
  • 中文后空格自动切换 inline 模式,输入大写字母自动切换 inline 模式。

其中在 Isearch 中使用,切换为 Rime 后需要再按一次 M-e 才可以正常使用。

init-meow.el

快捷键 功能 备注
M-S-< 移动光标到顶部 cmd-shift-<
;;; init-meow.el  --- Custom configuration
;;; Commentary
(require-package 'meow)
(require 'meow)
(defun meow-setup ()
  (setq meow-cheatsheet-layout meow-cheatsheet-layout-qwerty)
  (meow-motion-overwrite-define-key
   '("j" . meow-next)
   '("k" . meow-prev)
   '("<escape>" . ignore))
  (meow-leader-define-key
   ;; SPC j/k will run the original command in MOTION state.
   '("j" . "H-j")
   '("k" . "H-k")
   ;; Use SPC (0-9) for digit arguments.
   '("1" . meow-digit-argument)
   '("2" . meow-digit-argument)
   '("3" . meow-digit-argument)
   '("4" . meow-digit-argument)
   '("5" . meow-digit-argument)
   '("6" . meow-digit-argument)
   '("7" . meow-digit-argument)
   '("8" . meow-digit-argument)
   '("9" . meow-digit-argument)
   '("0" . meow-digit-argument)
   '("/" . meow-keypad-describe-key)
   '("?" . meow-cheatsheet))
  (meow-normal-define-key
   '("0" . meow-expand-0)
   '("9" . meow-expand-9)
   '("8" . meow-expand-8)
   '("7" . meow-expand-7)
   '("6" . meow-expand-6)
   '("5" . meow-expand-5)
   '("4" . meow-expand-4)
   '("3" . meow-expand-3)
   '("2" . meow-expand-2)
   '("1" . meow-expand-1)
   '("-" . negative-argument)
   '(";" . meow-reverse)
   '("," . meow-inner-of-thing)
   '("." . meow-bounds-of-thing)
   '("[" . meow-beginning-of-thing)
   '("]" . meow-end-of-thing)
   '("a" . meow-append)
   '("A" . meow-open-below)
   '("b" . meow-back-word)
   '("B" . meow-back-symbol)
   '("c" . meow-change)
   '("d" . meow-delete)
   '("D" . meow-backward-delete)
   '("e" . meow-next-word)
   '("E" . meow-next-symbol)
   '("f" . meow-find)
   '("g" . meow-cancel-selection)
   '("G" . meow-grab)
   '("h" . meow-left)
   '("H" . meow-left-expand)
   '("i" . meow-insert)
   '("I" . meow-open-above)
   '("j" . meow-next)
   '("J" . meow-next-expand)
   '("k" . meow-prev)
   '("K" . meow-prev-expand)
   '("l" . meow-right)
   '("L" . meow-right-expand)
   '("m" . meow-join)
   '("n" . meow-search)
   '("o" . meow-block)
   '("O" . meow-to-block)
   '("p" . meow-yank)
   '("q" . meow-quit)
   '("Q" . meow-goto-line)
   '("r" . meow-replace)
   '("R" . meow-swap-grab)
   '("s" . meow-kill)
   '("t" . meow-till)
   '("u" . meow-undo)
   '("U" . meow-undo-in-selection)
   '("v" . meow-visit)
   '("w" . meow-mark-word)
   '("W" . meow-mark-symbol)
   '("x" . meow-line)
   '("X" . meow-goto-line)
   '("y" . meow-save)
   '("Y" . meow-sync-grab)
   '("z" . meow-pop-selection)
   '("'" . repeat)
   '("<escape>" . ignore)))
(meow-setup)
(meow-global-mode 1)
;; 失去焦点时,退出 insert mode。
(add-hook 'focus-out-hook 'meow-insert-exit)
;; 延长数字显示时间
(setq meow-expand-hint-remove-delay 3)
(provide 'init-meow)
;;; init-meow.el ends here

beancount.el

GitHub - beancount/beancount-mode: Emacs major-mode to work with Beancount le…,这里是将其中的 beancount.el 下载到 .emacs.d/lisp/ 目录下,加上下列代码。

(add-to-list 'auto-mode-alist '("\\.beancount\\'" . beancount-mode))

init-epla.el

增加 use-packagestraight 的支持

;;; init-elpa.el --- Settings and helpers for package.el -*- lexical-binding: t -*-
;;; Commentary:
;;; Code:

(require 'package)
(require 'cl-lib)


;;; Install into separate package dirs for each Emacs version, to prevent bytecode incompatibility
(setq package-user-dir
      (expand-file-name (format "elpa-%s.%s" emacs-major-version emacs-minor-version)
                        user-emacs-directory))



;;; Standard package repositories

(add-to-list 'package-archives '( "melpa" . "https://melpa.org/packages/") t)
;; Official MELPA Mirror, in case necessary.
;;(add-to-list 'package-archives (cons "melpa-mirror" (concat proto "://www.mirrorservice.org/sites/melpa.org/packages/")) t)



;; Work-around for https://debbugs.gnu.org/cgi/bugreport.cgi?bug=34341
(when (and (version< emacs-version "26.3") (boundp 'libgnutls-version) (>= libgnutls-version 30604))
  (setq gnutls-algorithm-priority "NORMAL:-VERS-TLS1.3"))


;;; On-demand installation of packages

(defun require-package (package &optional min-version no-refresh)
  "Install given PACKAGE, optionally requiring MIN-VERSION.
If NO-REFRESH is non-nil, the available package lists will not be
re-downloaded in order to locate PACKAGE."
  (when (stringp min-version)
    (setq min-version (version-to-list min-version)))
  (or (package-installed-p package min-version)
      (let* ((known (cdr (assoc package package-archive-contents)))
             (best (car (sort known (lambda (a b)
                                      (version-list-<= (package-desc-version b)
                                                       (package-desc-version a)))))))
        (if (and best (version-list-<= min-version (package-desc-version best)))
            (package-install best)
          (if no-refresh
              (error "No version of %s >= %S is available" package min-version)
            (package-refresh-contents)
            (require-package package min-version t)))
        (package-installed-p package min-version))))

(defun maybe-require-package (package &optional min-version no-refresh)
  "Try to install PACKAGE, and return non-nil if successful.
In the event of failure, return nil and print a warning message.
Optionally require MIN-VERSION.  If NO-REFRESH is non-nil, the
available package lists will not be re-downloaded in order to
locate PACKAGE."
  (condition-case err
      (require-package package min-version no-refresh)
    (error
     (message "Couldn't install optional package `%s': %S" package err)
     nil)))


;;; Fire up package.el

(setq package-enable-at-startup nil)
(package-initialize)


;; package.el updates the saved version of package-selected-packages correctly only
;; after custom-file has been loaded, which is a bug. We work around this by adding
;; the required packages to package-selected-packages after startup is complete.

(defvar sanityinc/required-packages nil)

(defun sanityinc/note-selected-package (oldfun package &rest args)
  "If OLDFUN reports PACKAGE was successfully installed, note that fact.
The package name is noted by adding it to
`sanityinc/required-packages'.  This function is used as an
advice for `require-package', to which ARGS are passed."
  (let ((available (apply oldfun package args)))
    (prog1
        available
      (when available
        (add-to-list 'sanityinc/required-packages package)))))

(advice-add 'require-package :around 'sanityinc/note-selected-package)

(when (fboundp 'package--save-selected-packages)
  (require-package 'seq)
  (add-hook 'after-init-hook
            (lambda ()
              (package--save-selected-packages
               (seq-uniq (append sanityinc/required-packages package-selected-packages))))))


(require-package 'fullframe)
(fullframe list-packages quit-window)


(let ((package-check-signature nil))
  (require-package 'gnu-elpa-keyring-update))


(defun sanityinc/set-tabulated-list-column-width (col-name width)
  "Set any column with name COL-NAME to the given WIDTH."
  (when (> width (length col-name))
    (cl-loop for column across tabulated-list-format
             when (string= col-name (car column))
             do (setf (elt column 1) width))))

(defun sanityinc/maybe-widen-package-menu-columns ()
  "Widen some columns of the package menu table to avoid truncation."
  (when (boundp 'tabulated-list-format)
    (sanityinc/set-tabulated-list-column-width "Version" 13)
    (let ((longest-archive-name (apply 'max (mapcar 'length (mapcar 'car package-archives)))))
      (sanityinc/set-tabulated-list-column-width "Archive" longest-archive-name))))

(add-hook 'package-menu-mode-hook 'sanityinc/maybe-widen-package-menu-columns)

;; Bootstrap `use-package'
(when (version< emacs-version "29.0")
  (unless (package-installed-p 'use-package)
    (package-refresh-contents)
    (package-install 'use-package))
  (eval-and-compile
    (setq use-package-always-ensure nil)
    (setq use-package-always-defer nil)
    (setq use-package-always-demand nil)
    (setq use-package-expand-minimally nil)
    (setq use-package-enable-imenu-support t))
  (eval-when-compile
    (require 'use-package)))

;; Install straight.el
(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 5))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

(provide 'init-elpa)
;;; init-elpa.el ends here

init-local.el

init.el 中有如下代码。

;; Allow users to provide an optional "init-local" containing personal settings
(require 'init-local nil t)

默认没有 init-local.org 这个文件,创建一个,就可以往里面塞自己的配置了。

Consult

Purcell 中已经包含了这个 package,也有 consult-ripgrep 这个方法。需要安装 ripgrep​。

brew install ripgrep
(defun bms/org-roam-rg-search ()
  "Search org-roam directory using consult-ripgrep. With live-preview."
  (interactive)
  (let ((consult-ripgrep-command "rg --null --ignore-case --type org --line-buffered --color=always --max-columns=500 --no-heading --line-number . -e ARG OPTS"))
    (consult-ripgrep org-roam-directory)))

(global-set-key (kbd "C-c rr") 'bms/org-roam-rg-search)

搜索 org-roam-directory 下的内容。

Deft

用作一些不想放在 org-roam 中的一些笔记的操作。

(require-package 'deft)
(setq deft-extensions '("md" "tex" "org" "mw" "conf"))
(setq deft-directory "~/Dropbox/org/")
(setq deft-recursive t)

(advice-add 'deft-parse-title :override #'cm/deft-parse-title)

(setq deft-strip-summary-regexp
(concat "\\("
  "[\n\t]" ;; blank
  "\\|^#\\+[[:alpha:]_]+:.*$" ;; org-mode metadata
  "\\|^#\s[-]*$" ;; org-mode metadata
  "\\|^:PROPERTIES:\n\\(.+\n\\)+:END:\n"
  "\\)"))

(global-set-key [f7] 'deft)
(global-set-key (kbd "C-x C-g") 'deft-find-file)

;; deft parse title
(defun cm/deft-parse-title (file contents)
  "Parse the given FILE and CONTENTS and determine the title.
If `deft-use-filename-as-title' is nil, the title is taken to
be the first non-empty line of the FILE.  Else the base name of the FILE is
used as title."
  (let ((begin (string-match "^#\\+[tT][iI][tT][lL][eE]: .*$" contents)))
    (if begin
        (string-trim (substring contents begin (match-end 0)) "#\\+[tT][iI][tT][lL][eE]: *" "[\n\t ]+")
      (deft-base-filename file))))

Ox-hugo

导出到 Hugo,方便与他人分享内容。

(require-package 'ox-hugo)
(with-eval-after-load 'ox
  (require 'ox-hugo))

导出到 Hugo 还需要在文件头部添加

#+HUGO_BASE_DIR: ~/Dropbox/hugo/
#+HUGO_SECTION: posts/main
#+HUGO_WEIGHT: auto
#+HUGO_AUTO_SET_LASTMOD: t

需要到处为 <mark></mark>1

#+header: :trim-pre nil :trim-post nil
#+begin_mark
marked text
#+end_mark

init-latex.el

;;; init-latex.el  --- Custom configuration
;;; Commentary
(require-package 'auctex)
(require-package 'cdlatex)
(require-package 'cdlatex)
(add-hook 'LaTeX-mode-hook 'turn-on-cdlatex)
;; 导出中文 PDF
;; 需要将 elegantpaper.cls 文件放在 org 目录下
;; 需要安装依赖 brew install pygments
;; org 文件头部增加
;; #+LATEX_COMPILER: xelatex
;; #+LATEX_CLASS: elegantpaper
;; #+OPTIONS: prop:t
(with-eval-after-load 'ox-latex
  ;; http://orgmode.org/worg/org-faq.html#using-xelatex-for-pdf-export
  ;; latexmk runs pdflatex/xelatex (whatever is specified) multiple times
  ;; automatically to resolve the cross-references.
  (setq org-latex-pdf-process '("latexmk -xelatex -quiet -shell-escape -f %f"))
  (add-to-list 'org-latex-classes
               '("elegantpaper"
                 "\\documentclass[lang=cn]{elegantpaper}
                 [NO-DEFAULT-PACKAGES]
                 [PACKAGES]
                 [EXTRA]"
                 ("\\section{%s}" . "\\section*{%s}")
                 ("\\subsection{%s}" . "\\subsection*{%s}")
                 ("\\subsubsection{%s}" . "\\subsubsection*{%s}")
                 ("\\paragraph{%s}" . "\\paragraph*{%s}")
                 ("\\subparagraph{%s}" . "\\subparagraph*{%s}")))
  (setq org-latex-listings 'minted)
  (add-to-list 'org-latex-packages-alist '("cache=false" "minted" t)))
(setq org-preview-latex-default-process 'dvisvgm)
;; xenops
(require-package 'xenops)
(add-hook 'LaTeX-mode-hook #'xenops-mode)
(with-eval-after-load 'xenops
  (setq xenops-math-image-scale-factor 1.3
        xenops-image-try-write-clipboard-image-to-file nil
        xenops-reveal-on-entry nil
        xenops-math-image-margin 0
        xenops-math-latex-max-tasks-in-flight 16
        xenops-auctex-electric-insert-commands nil)
  (defun eli/change-xenops-latex-header (orig &rest args)
    (let ((org-format-latex-header "\\documentclass[dvisvgm,preview]{standalone}\n\\usepackage{arev}\n\\usepackage{color}\n[PACKAGES]\n[DEFAULT-PACKAGES]"))
      (apply orig args)))
  (advice-add 'xenops-math-latex-make-latex-document :around #'eli/change-xenops-latex-header)
  (advice-add 'xenops-math-file-name-static-hash-data :around #'eli/change-xenops-latex-header)

  (defun eli/delete-region ()
    (if (use-region-p)
        (delete-region (region-beginning)
                       (region-end))))
  (advice-add 'xenops-handle-paste-default
              :before #'eli/delete-region)

  (defun eli/xenops-math-add-cursor-sensor-property ()
    (-when-let* ((element (xenops-math-parse-element-at-point)))
      (let ((beg (plist-get element :begin))
            (end (plist-get element :end))
            (props '(cursor-sensor-functions (xenops-math-handle-element-transgression))))
        (add-text-properties beg end props)
        (add-text-properties (1- end) end '(rear-nonsticky (cursor-sensor-functions))))))
  (advice-add 'xenops-math-add-cursor-sensor-property :override #'eli/xenops-math-add-cursor-sensor-property)

  ;; Vertically align LaTeX preview in org mode
  (setq xenops-math-latex-process-alist
        '((dvisvgm :programs
                   ("latex" "dvisvgm")
                   :description "dvi > svg" :message "you need to install the programs: latex and dvisvgm." :image-input-type "dvi" :image-output-type "svg" :image-size-adjust
                   (1.7 . 1.5)
                   :latex-compiler
                   ("latex -interaction nonstopmode -shell-escape -output-format dvi -output-directory %o %f")
                   :image-converter
                   ("dvisvgm %f -n -e -b 1 -c %S -o %O"))))

  ;; from https://list.orgmode.org/874k9oxy48.fsf@gmail.com/#Z32lisp:org.el
  (defun eli/org--match-text-baseline-ascent (imagefile)
    "Set `:ascent' to match the text baseline of an image to the surrounding text.
Compute `ascent' with the data collected in IMAGEFILE."
    (let* ((viewbox (split-string
                     (xml-get-attribute (car (xml-parse-file imagefile)) 'viewBox)))
           (min-y (string-to-number (nth 1 viewbox)))
           (height (string-to-number (nth 3 viewbox)))
           (ascent (round (* -100 (/ min-y height)))))
      (if (or (< ascent 0) (> ascent 100))
          'center
        ascent)))

  (defun eli/xenops-preview-align-baseline (element &rest _args)
    "Redisplay SVG image resulting from successful LaTeX compilation of ELEMENT.
Use the data in log file (e.g. \"! Preview: Snippet 1 ended.(368640+1505299x1347810).\")
to calculate the decent value of `:ascent'. "
    (let* ((inline-p (eq 'inline-math (plist-get element :type)))
           (ov-beg (plist-get element :begin))
           (ov-end (plist-get element :end))
           (cache-file (car (last _args)))
           (ov (car (overlays-at (/ (+ ov-beg ov-end) 2) t)))
           img new-img ascent)
      (when (and ov inline-p)
        (setq ascent (+ 1 (eli/org--match-text-baseline-ascent cache-file)))
        (setq img (cdr (overlay-get ov 'display)))
        (setq new-img (plist-put img :ascent ascent))
        (overlay-put ov 'display (cons 'image new-img)))))
  (advice-add 'xenops-math-display-image :after
              #'eli/xenops-preview-align-baseline)

  ;; from: https://kitchingroup.cheme.cmu.edu/blog/2016/11/06/
  ;; Justifying-LaTeX-preview-fragments-in-org-mode/
  ;; specify the justification you want
  (plist-put org-format-latex-options :justify 'right)

  (defun eli/xenops-justify-fragment-overlay (element &rest _args)
    (let* ((ov-beg (plist-get element :begin))
           (ov-end (plist-get element :end))
           (ov (car (overlays-at (/ (+ ov-beg ov-end) 2) t)))
           (position (plist-get org-format-latex-options :justify))
           (inline-p (eq 'inline-math (plist-get element :type)))
           width offset)
      (when (and ov
                 (imagep (overlay-get ov 'display)))
        (setq width (car (image-display-size (overlay-get ov 'display))))
        (cond
         ((and (eq 'right position)
               (not inline-p)
               (> width 50))
          (setq offset (floor (- fill-column
                                 width)))
          (if (< offset 0)
              (setq offset 0))
          (overlay-put ov 'before-string (make-string offset ? )))
         ((and (eq 'right position)
               (not inline-p))
          (setq offset (floor (- (/ fill-column 2)
                                 (/ width 2))))
          (if (< offset 0)
              (setq offset 0))
          (overlay-put ov 'before-string (make-string offset ? )))))))
  (advice-add 'xenops-math-display-image :after
              #'eli/xenops-justify-fragment-overlay)


  ;; from: https://kitchingroup.cheme.cmu.edu/blog/2016/11/07/
  ;; Better-equation-numbering-in-LaTeX-fragments-in-org-mode/
  (defun eli/xenops-renumber-environment (orig-func element latex colors
                                                    cache-file display-image)
    (let ((results '())
          (counter -1)
          (numberp)
          (outline-regexp org-outline-regexp))
      (setq results (cl-loop for (begin .  env) in
                             (org-element-map (org-element-parse-buffer)
                                 'latex-environment
                               (lambda (env)
                                 (cons
                                  (org-element-property :begin env)
                                  (org-element-property :value env))))
                             collect
                             (cond
                              ((and (string-match "\\\\begin{equation}" env)
                                    (not (string-match "\\\\tag{" env)))
                               (cl-incf counter)
                               (cons begin counter))
                              ((and (string-match "\\\\begin{align}" env)
                                    (string-match "\\\\notag" env))
                               (cl-incf counter)
                               (cons begin counter))
                              ((string-match "\\\\begin{align}" env)
                               (prog2
                                   (cl-incf counter)
                                   (cons begin counter)
                                 (with-temp-buffer
                                   (insert env)
                                   (goto-char (point-min))
                                   ;; \\ is used for a new line. Each one leads
                                   ;; to a number
                                   (cl-incf counter (count-matches "\\\\$"))
                                   ;; unless there are nonumbers.
                                   (goto-char (point-min))
                                   (cl-decf counter
                                            (count-matches "\\nonumber")))))
                              (t
                               (cons begin nil)))))
      (when (setq numberp (cdr (assoc (plist-get element :begin) results)))
        (setq latex
              (concat
               (format "\\setcounter{equation}{%s}\n" numberp)
               latex))))
    (funcall orig-func element latex colors cache-file display-image))
  (advice-add 'xenops-math-latex-create-image
              :around #'eli/xenops-renumber-environment))

(autoload #'mathpix-screenshot "mathpix" nil t)
(with-eval-after-load 'mathpix
  (let ((n (random 4)))
    (setq mathpix-app-id (with-temp-buffer
                           (insert-file-contents
                            "~/.emacs.d/private/mathpix-app-id")
                           (nth n (split-string (buffer-string) "\n")))
          mathpix-app-key (with-temp-buffer
                            (insert-file-contents
                             "~/.emacs.d/private/mathpix-app-key")
                            (nth n (split-string (buffer-string) "\n")))))
  (setq mathpix-screenshot-method "flameshot gui --raw > %s"))
(provide 'init-latex)
;;; init-latex.el ends here

离线分享的时候最大程度的保持原貌的方法就是 PDF。

需要安装 texlive

brew install texlive

Latex 语法可以参考:Latex

导出

导出含有中文时是需要配置 CJK,ElegantPaper 可以跨过这一步设置,只需要将 elegantpaper.cls 放到需要导出 PDF 的目录下,同时安装依赖。

brew install pygments

在导出文件头部增加

#+LATEX_COMPILER: xelatex
#+LATEX_CLASS: elegantpaper
#+OPTIONS: prop:t

预览

预览需要安装 firamath 字体,可以有更好的展示效果。另外 dvisvgm 应该在 texlive 中已经包含才对,但是依旧找不到。这里我重新从源码编译了一份。

init-corfu.el

增加了 kind-icon

;;; init-corfu.el --- Interactive completion in buffers -*- lexical-binding: t -*-
;;; Commentary:
;;; Code:

;; WAITING: haskell-mode sets tags-table-list globally, breaks tags-completion-at-point-function
;; TODO Default sort order should place [a-z] before punctuation

(setq tab-always-indent 'complete)
(when (maybe-require-package 'orderless)
  (with-eval-after-load 'vertico
    (require 'orderless)
    (setq completion-styles '(orderless basic))))
(setq completion-category-defaults nil
      completion-category-overrides nil)
(setq completion-cycle-threshold 4)

(when (maybe-require-package 'corfu)
  (setq-default corfu-auto t)
  (with-eval-after-load 'eshell
    (add-hook 'eshell-mode-hook (lambda () (setq-local corfu-auto nil))))
  (setq-default corfu-quit-no-match 'separator)
  (add-hook 'after-init-hook 'global-corfu-mode)

  (when (featurep 'corfu-popupinfo)
    (with-eval-after-load 'corfu (corfu-popupinfo-mode)))

  (when (maybe-require-package 'kind-icon)
    ; Enable `kind-icon'
    (with-eval-after-load 'corfu
      (add-to-list 'corfu-margin-formatters #'kind-icon-margin-formatter)
      ))
  )


(provide 'init-corfu)
;;; init-corfu.el ends here

在切换主题的时候,​kind-icon 会有残留,可以执行 M-x kind-icon-reset-cache 清除 cache 即可。

init-bibtex.el

管理 Bibtex 可以方便在文章中引用,以及新增读书笔记,结合 agenda 更好的管理读书条目以及进度。

其中自定义了笔记模板和读书列表模板,前者是增加了 citation key 作为 roam ref,org ID 的生成以及时间戳;后者则是将默认的 TODO 关键字换成了 PROJECT 关键字,并将 active timestamps 改为 inactive timestamps,方便计算每个 PROJECT 创建了多长时间展示在 agenda 当中。

;;; init-ebib.el  --- Custom configuration
;;; Commentary
(use-package bibtex
  :defer t
  :config
  (setq bibtex-file-path "~/Dropbox/org/bib/"
        bibtex-files '("bibtex.bib")
        bibtex-notes-path "~/Dropbox/org/main/"

        bibtex-align-at-equal-sign t
        bibtex-autokey-titleword-separator "-"
        bibtex-autokey-year-title-separator "-"
        bibtex-autokey-name-year-separator "-"
        bibtex-dialect 'biblatex))

(use-package ebib
  :straight t
  :commands ebib-zotero-protocol-handler
  :init
  :config
  (setq ebib-default-directory bibtex-file-path
        ebib-bib-search-dirs `(,bibtex-file-path)
        ebib-file-search-dirs `(,(concat bibtex-file-path "files/"))
        ebib-notes-directory bibtex-notes-path
        ebib-reading-list-file "~/Dropbox/org/agenda/books.org"

        ebib-bibtex-dialect bibtex-dialect
        ebib-file-associations '(("pdf" . "open"))
        ebib-index-default-sort '("timestamp" . descend)
        ;; 笔记模板
        ebib-notes-template ":PROPERTIES:\n:ID: %i\n:ROAM_REFS: @%k\n:END:\n#+title: %t\n#+description: %d\n#+date: %s\n%%?\n"
        ebib-notes-template-specifiers '((?k . ebib-create-key)
                                         (?i . ebib-create-id)
                                         (?t . ebib-create-org-title)
                                         (?d . ebib-create-org-description)
                                         (?l . ebib-create-org-link)
                                         (?s . ebib-create-org-time-stamp))
        ;; 读书列表模板
        ebib-reading-list-template "* %M %T\n:PROPERTIES:\n%K\n:END:\n%F\n%S\n"
        ebib-reading-list-template-specifiers '((?M . ebib-reading-list-project-marker)
                                                (?T . ebib-create-org-title)
                                                (?K . ebib-reading-list-create-org-identifier)
                                                (?F . ebib-create-org-file-link)
                                                (?S . ebib-create-org-stamp-inactive))
        ebib-preload-bib-files bibtex-files
        ebib-use-timestamp t)

  (defun ebib-create-key (key _db)
    "Return the KEY in DB for the Org mode note."
    (format "%s" key))

  (defun ebib-create-id (_key _db)
    "Create an ID for the Org mode note."
    (org-id-new))

  (defun ebib-create-org-time-stamp (_key _db)
    "Create timestamp for the Org mode note."
    (format "%s" (with-temp-buffer (org-insert-time-stamp nil))))

  ;; 替换官方的 ebib-reading-list-todo-marker
  (defcustom ebib-reading-list-project-marker "PROJECT"
    "Marker for reading list items that are still open."
    :group 'ebib-reading-list
    :type '(string :tag "Project marker"))
  ;; 获取 [%Y-%m-%d %a %H:%M] 格式的时间戳
  (defun ebib-create-org-stamp-inactive (_key _db)
  "Create inactive timestamp for the Org mode note."
  (let ((org-time-stamp-custom-formats org-time-stamp-custom-formats))
    (format "%s" (with-temp-buffer (org-time-stamp-inactive nil))))))

(require-package 'citar)
(use-package citar
  :no-require
  :custom
  (org-cite-global-bibliography '("~/Dropbox/org/bib/bibtex.bib"))
  (citar-notes-paths (list "~/Dropbox/org/main"))
  (citar-library-paths (list "~/Dropbox/org/bib/files"))
  (org-cite-insert-processor 'citar)
  (org-cite-follow-processor 'citar)
  (org-cite-activate-processor 'citar)
  (citar-bibliography org-cite-global-bibliography)
  ;; optional: org-cite-insert is also bound to C-c C-x C-@
  :bind
  (:map org-mode-map :package org ("C-c b" . #'org-cite-insert)))
(provide 'init-bibtex)
;;; init-bibtex.el ends here

搜索条目可以用 & 来搜索,参考 Ebib Manual

init-themes.el

用的 Screen shots of the Ef themes for GNU Emacs | Protesilaos Stavrou,​ef-spring 作为非编码时的主题,​ef-night 作为编码时的主题。另外设置背景透明以及光标所在代码块按照括号高亮的函数也在放在这里。

;;; init-themes.el --- Defaults for themes -*- lexical-binding: t -*-
;;; Commentary:
;;; Code:

(require-package 'ef-themes)
(require-package 'gruvbox-theme)

;; Don't prompt to confirm theme safety. This avoids problems with
;; first-time startup on Emacs > 26.3.
(setq custom-safe-themes t)

;; If you don't customize it, this is the theme you get.
(setq-default custom-enabled-themes '(ef-spring))

;; Ensure that themes will be applied even if they have not been customized
(defun reapply-themes ()
  "Forcibly load the themes listed in `custom-enabled-themes'."
  (dolist (theme custom-enabled-themes)
    (unless (custom-theme-p theme)
      (load-theme theme)))
  (custom-set-variables `(custom-enabled-themes (quote ,custom-enabled-themes))))

(add-hook 'after-init-hook 'reapply-themes)


;; Toggle between light and dark

(defun light ()
  "Activate a light color theme."
  (interactive)
  (disable-theme (car custom-enabled-themes))
  (setq custom-enabled-themes '(ef-spring))
  (reapply-themes))

(defun dark ()
  "Activate a dark color theme."
  (interactive)
  (disable-theme (car custom-enabled-themes))
  (setq custom-enabled-themes '(gruvbox))
  (reapply-themes))


(when (maybe-require-package 'dimmer)
  (setq-default dimmer-fraction 0.15)
  (add-hook 'after-init-hook 'dimmer-mode)
  (with-eval-after-load 'dimmer
    ;; TODO: file upstream as a PR
    (advice-add 'frame-set-background-mode :after (lambda (&rest args) (dimmer-process-all))))
  (with-eval-after-load 'dimmer
    ;; Don't dim in terminal windows. Even with 256 colours it can
    ;; lead to poor contrast.  Better would be to vary dimmer-fraction
    ;; according to frame type.
    (defun sanityinc/display-non-graphic-p ()
      (not (display-graphic-p)))
    (add-to-list 'dimmer-exclusion-predicates 'sanityinc/display-non-graphic-p)))

;; fake transparency
(defun user/change-background-opacity (alpha)
  (interactive "nOpacity: ")
  (if (and (>= alpha 0) (<= alpha 100))
      (user/set-background-opacity alpha)
    (format-message "Opacity has to be between 0 and 100 but is %d." alpha)))

(defun user/set-background-opacity (alpha)
  (set-frame-parameter (selected-frame) 'alpha alpha)
  (add-to-list 'default-frame-alist (cons 'alpha alpha)))

;; (user/set-background-opacity 90)

;; true transparency
;; (defun kb/toggle-window-transparency ()
;;   "Toggle transparency."
;;   (interactive)
;;   (let ((alpha-transparency 75))
;;     (pcase (frame-parameter nil 'alpha-background)
;;       (alpha-transparency (set-frame-parameter nil 'alpha-background 100))
;;       (t (set-frame-parameter nil 'alpha-background alpha-transparency)))))
;; (kb/toggle-window-transparency)
(provide 'init-themes)
;;; init-themes.el ends here

init-elfeed.el

;;; init-elfeed.el  --- Custom configuration
;;; Commentary
(require-package 'elfeed)
(global-set-key (kbd "C-x w") 'elfeed)

(use-package elfeed-org
  :ensure t
  :config
  (elfeed-org)
  (setq rmh-elfeed-org-files (list "~/Dropbox/org/elfeed.org")))
(defun concatenate-authors (authors-list)
  "Given AUTHORS-LIST, list of plists; return string of all authors
concatenated."
  (mapconcat
   (lambda (author) (plist-get author :name))
   authors-list ", "))
(defun my-search-print-fn (entry)
  "Print ENTRY to the buffer."
  (let* ((date (elfeed-search-format-date (elfeed-entry-date entry)))
         (title (or (elfeed-meta entry :title)
                    (elfeed-entry-title entry) ""))
         (title-faces (elfeed-search--faces (elfeed-entry-tags entry)))
         (feed (elfeed-entry-feed entry))
         (feed-title
          (when feed
            (or (elfeed-meta feed :title) (elfeed-feed-title feed))))
         (entry-authors (concatenate-authors
                         (elfeed-meta entry :authors)))
         (tags (mapcar #'symbol-name (elfeed-entry-tags entry)))
         (tags-str (mapconcat
                    (lambda (s) (propertize s 'face
                                            'elfeed-search-tag-face))
                    tags ","))
         (title-width (- (window-width) 10
                         elfeed-search-trailing-width))
         (title-column (elfeed-format-column
                        title (elfeed-clamp
                               elfeed-search-title-min-width
                               title-width
                               elfeed-search-title-max-width)
                        :left))
         (authors-width 135)
         (authors-column (elfeed-format-column
                        entry-authors (elfeed-clamp
                               elfeed-search-title-min-width
                               authors-width
                               131)
                        :left)))

    (insert (propertize date 'face 'elfeed-search-date-face) " ")

    (insert (propertize title-column
                        'face title-faces 'kbd-help title) " ")

    (insert (propertize authors-column
                        'face 'elfeed-search-date-face
                        'kbd-help entry-authors) " ")

    ;; (when feed-title
    ;;   (insert (propertize entry-authors
    ;; 'face 'elfeed-search-feed-face) " "))

    (when entry-authors
      (insert (propertize feed-title
                          'face 'elfeed-search-feed-face) " "))

    ;; (when tags
    ;;   (insert "(" tags-str ")"))

    )
  )
(setq elfeed-search-print-entry-function #'my-search-print-fn)
(provide 'init-elfeed)
;;; init-elfeed.el ends here

init-telega.el

Building TDLib

brew install tdlib 的版本过低,需要自行编译,参考 TDLib build instructions 。这个之后需要 M-x telega-server-build 重新加载 telega-server。

xcode-select --install
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install gperf cmake openssl
git clone https://github.com/tdlib/td.git
cd td
rm -rf build
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl/ -DCMAKE_INSTALL_PREFIX:PATH=/usr/local ..
cmake --build . --target install
cd ..
cd ..
ls -l /usr/local

如果报错 "user-error: TDLib is not installed into "/usr/local". Set ‘telega-server-libs-prefix’ to the TDLib installion path"​,则可以通过 M-: (setq telega-server-libs-prefix “/path/to/tdlib/install/path”) RET 然后 M-x telega-server-build RET 重新构建。

Running telega-server in docker

brew install --cask docker
brew install ffmpeg
docker pull zevlg/telega-server:latest

在配置中加上下面代码即可。

(setq telega-use-docker t)
快捷键 功能 备注
C-c C-v 发送图片 brew install pngpaste
! 点赞
C-c C-a Attachment type 其中 markup 可以发送 md 等格式
C-c C-c 光标移到消息后,按快捷键取消搜索。
M-g r 跳转到最新消息 或者 M-g >

头像裂开的问题​:是由于字体高度问题导致的,安装 init-font.el 中的设置,​Iosevka 大多数中文字体配合,并设置 (add-to-list 'face-font-rescale-alist '("Apple Color Emoji" . 0.8)) 可以实现头像不裂开。但由于 Emoji 以及各种昵称中的特殊字符,比如 Noto Sans Egyptian Hieroglyphs 这种古埃及象形文字,以及 Noto Sans Kannada 和 STIXGeneral 等字体,出现在昵称以及 Reply 行中,都会导致头像裂开。

有几种解决的方案。

  1. 使用更纱黑体(字体太丑且并不能兼容那些使用第三点当中提到的特殊字体的情况)。
(defun telega-buffer-face-mode-variable ()
  (interactive)
  (make-face 'my-telega-face)
  ;; (set-face-attribute 'my-telega-face nil :font "M+ 1m")
  (set-face-attribute 'my-telega-face nil :font "Sarasa Mono SC Nerd 13")

  (setq buffer-face-mode-face 'my-telega-face)
  (buffer-face-mode))

(add-hook 'telega-root-mode-hook 'telega-buffer-face-mode-variable)
(add-hook 'telega-webpage-mode-hook 'telega-buffer-face-mode-variable)
(add-hook 'telega-chat-mode-hook 'telega-buffer-face-mode-variable)
  1. 使用 (setf (alist-get 2 telega-avatar-factors-alist) '(0.5 . 0.1)) 来使得本来两行显示的头像缩小至一行显示(头像显示几乎废了)。
  2. 在基础字号的基础上(这里是 14)调整特殊字符/字体的缩放就可以完美解决头像裂开的问题了,完整代码如下。

contrib

telega 中有个 contrib 的子目录,当中有一些挺实用的功能。这里启用了三种,过滤广告、代码高亮以及短链。

原生的加载子目录。

(push (expand-file-name "contrib" (file-name-directory (locate-library "telega"))) load-path)

使用 straight 的话如下。

:straight (:host github :repo "zevlg/telega.el"
           :files (:defaults "contrib"))

Animated Stickers

git clone https://github.com/zevlg/tgs2png.git
git submodule init
git submodule update
mkdir build
cd build
cmake ..
make
copy tgs2png somewhere into $PATH

这里我编译完就已经是可用的了。

display-buffer-alist

每次打开 telega 都是占了 emacs 窗口的一半(emacs 我都是全屏使用),需要手动去调整 telega 的窗口。这里设置了一下 display-buffer-alist 之后就可以固定 telega 的窗口在右侧,并且占据窗口的 35% 的宽度。

(add-hook 'telega-mode-hook
          (lambda ()
            (display-buffer (current-buffer) '((display-buffer-in-side-window)))))

(setq display-buffer-alist
      '(("\\*Telega Root\\*" . ((display-buffer-in-side-window)
                               (window-width . 0.35)
                               (side . right)
                               (slot . 0)
                               (dedicated . nil)))))

上述代码除了 dedicated 属性,都是由 ChatGPT Plus 生成。不设置该字段为 nil 会导致从 Telega 列表进入聊天 buffer 后重新打开新的 buffer,具体原因参照 dedicated 字段的解释参见 Displaying Buffers in Side Windows (GNU Emacs Lisp Reference Manual)

最后总的配置如下。

;;; init-telega.el  --- Custom configuration
;;; Commentary
(require-package 'telega)
(require-package 'all-the-icons)
;; If language-detection is available,
;; then laguage could be detected automatically
;; for code blocks without language explicitly specified.
(require-package 'language-detection)
;; 加载子文件夹 contrib
(push (expand-file-name "contrib" (file-name-directory (locate-library "telega"))) load-path)
(require 'telega-mnz)
(global-telega-mnz-mode 1)
(setq telega-debug t)
;; 自动播放 gif
(telega-autoplay-mode 1)
(define-key global-map (kbd "C-c t") telega-prefix-map)

;; 去除昵称、回复行的背景高亮
(defun my-telega-chat-mode ()
  ;; telega-msg-inline-reply   ⊆ telega-msg-heading
  ;; telega-msg-inline-forward ⊆ telega-msg-heading
  (set-face-attribute 'telega-msg-heading nil
                      :underline nil
                      :inherit nil
  ;; Warning: setting attribute ‘:background’ of face ‘telega-msg-heading’: nil value is invalid, use ‘unspecified’ instead.
                      :background 'unspecified)
  (set-face-attribute 'telega-msg-inline-reply nil
                      :foreground "#86C166") ;; 苗 NAE
  (set-face-attribute 'telega-msg-inline-forward nil
                      :foreground "#FFB11B")
  (set-face-attribute 'telega-entity-type-mention nil
                      :underline '(:style line)
                      :weight 'bold)
  (set-face-attribute 'telega-msg-self-title nil
                     :foreground "#E2943B") ;; 朽葉 KUCHIBA
  (set-face-attribute 'telega-msg-user-title nil
                      :weight 'bold)
  (set-face-attribute 'telega-button nil
                      :foreground "#986DB2"
                      :box '(:line-width (-2 . -2) :color "#986DB2" :style nil))
  (set-face-attribute 'telega-button-active nil
                      :foreground "#ffffff"
                      :background "#986DB2")
  ;; (setq telega-chat-prompt-format "🏄🏻〉 ")
  ;; 缩小 emoji 及特殊字符/字体显示,防止头像裂开。
  ;; (add-to-list 'face-font-rescale-alist '("Apple Color Emoji" . 0.8))
  (add-to-list 'face-font-rescale-alist '("HanaMinA" . 0.9))
  (add-to-list 'face-font-rescale-alist '("Noto Sans Egyptian Hieroglyphs" . 0.675))
  (add-to-list 'face-font-rescale-alist '("STIXGeneral" . 0.675))
  (add-to-list 'face-font-rescale-alist '("Apple Symbols" . 0.675))
  (add-to-list 'face-font-rescale-alist '("Noto Sans Kannada" . 0.675))
  (add-to-list 'face-font-rescale-alist '("Academy Engraved LET" . 0.675))
  (add-to-list 'face-font-rescale-alist '("Kohinoor Devanagari" . 0.675))
  (add-to-list 'face-font-rescale-alist '("Geneva" . 0.675))
  (add-to-list 'face-font-rescale-alist '("Noto Sans Oriya" . 0.675)))
(add-hook 'telega-chat-mode-hook 'my-telega-chat-mode)
;; 未读提示
(set-face-attribute 'telega-unmuted-count nil
                    :foreground "#FFB11B"
                    :weight 'bold)
(set-face-attribute 'telega-mention-count nil
                    :foreground "#FE6DB3")
(set-face-attribute 'telega-muted-count nil
                    :foreground "#86C166"
                    :weight 'bold)
;; 多加一条横线在输入和聊天页面之间
(setq telega-symbol-underline-bar
      (propertize " " 'face 'telega-webpage-strike-through))
;; 聊天列表高亮
(defun lg-telega-root-mode ()
  (hl-line-mode 1))

(defun lg-telega-chat-update (chat)
  (with-telega-root-buffer
    (hl-line-highlight)))
(add-hook 'telega-chat-update-hook 'lg-telega-chat-update)
(add-hook 'telega-root-mode-hook 'lg-telega-root-mode)
;; Linux settings
(when *IS-LINUX*
  (setq telega-root-show-avatars nil)
  (setq telega-user-show-avatars nil)
  (setq telega-chat-show-avatars nil)
  (setq telega-proxies
        (list '(:server "127.0.0.1" :port 7890 :enable t
                        :type (:@type "proxyTypeSocks5"
                                      :username "" :password ""))
              )))
;; Opening files using external programs
(if *IS-MAC*
    (setcdr (assq t org-file-apps-gnu) 'browse-url-default-macosx-browser)
  (setcdr (assq t org-file-apps-gnu) 'browse-url-xdg-open))
(setq telega-open-file-function 'org-open-file)
;; 固定 telega 窗口在右侧
(add-hook 'telega-mode-hook
          (lambda ()
            (display-buffer (current-buffer) '((display-buffer-in-side-window)))))

(setq display-buffer-alist
      '(("\\*Telega Root\\*" . ((display-buffer-in-side-window)
                               (window-width . 0.35)
                               (side . right)
                               (slot . 0)
                               (dedicated . nil)))))

(provide 'init-telega)
;;; init-telega.el ends here

init-translate.el

;;; init-translate.el  --- Custom configuration
;;; Commentary
(require-package 'go-translate)
(use-package go-translate
  :bind (("C-c t g" . gts-do-translate)
         ("C-c t p" . go-translate-at-point)
         ("C-c t s" . go-translate-save-kill-ring))
  :config
  (setq gts-translate-list '(("en" "zh")))
  (setq gts-default-translator
        (gts-translator
         :picker (gts-prompt-picker)
         :engines (list (gts-bing-engine) (gts-google-engine))
         :render (gts-buffer-render)))

  ;; Pick directly and use Google RPC API to translate
  (defun go-translate-at-point ()
    (interactive)
    (gts-translate (gts-translator
                    :picker (gts-noprompt-picker)
                    :engines (gts-google-rpc-engine)
                    :render (gts-buffer-render))))

  ;; Pick directly and add the results into kill-ring
  (defun go-translate-save-kill-ring ()
    (interactive)
    (gts-translate (gts-translator
                    :picker (gts-noprompt-picker)
                    :engines (gts-google-engine
                              :parser (gts-google-summary-parser))
                    :render (gts-kill-ring-render))))
  (set-face-attribute 'gts-render-buffer-me-header-backgroud-face nil :background "#7BA23F"))
(provide 'init-translate)
;;; init-translate.el ends here

init-org-modern.el

;;; init-org-modern.el  --- Custom configuration
;;; Commentary
(require-package 'org-modern)
(use-package org-modern
  :after org
  :hook (org-mode . org-modern-mode)
  :init
  (menu-bar-mode -1)
  (tool-bar-mode -1)
  (scroll-bar-mode -1)
  (setq org-modern-star ["➫" "✦" "✜" "✲" "✸" "❅"]
        org-hide-emphasis-markers t
        org-tags-column 0
        org-catch-invisible-edits 'show-and-error
        org-special-ctrl-a/e t
        org-insert-heading-respect-content t
        org-modern-table-horizontal 0.2
        org-modern-list '((43 . "➤")
                          (45 . "▻")
                          (42 . "►"))
        org-ellipsis "[+]"))
  (modify-all-frames-parameters
   '((right-divider-width . 5)
     (left-divider-width . 5)
     (internal-border-width . 5)))
  (dolist (face '(window-divider
                  window-divider-first-pixel
                  window-divider-last-pixel))
    (face-spec-reset-face face)
    (set-face-foreground face (face-attribute 'default :background)))
  (set-face-background 'fringe (face-attribute 'default :background))
(custom-set-faces
 '(org-ellipsis ((t (:foreground "#90B44B"))))) ;; 鶸萌黄
(provide 'init-org-modern)
;;; init-org-modern.el ends here

init-quick-menus.el

Daily Log 是按照每天的日期生成,并不是固定的文件名,因此这里写了自定义方法去跳转到今天/昨天的 Daily Log,方便用来回顾。

;;; init-quick-menus.el  --- Custom configuration
;;; Commentary
;; org-starter
(require-package 'org-starter)
(setq org-refile-use-outline-path 'file)
(use-package org-starter
  :config
  ;; (add-hook! 'after-init-hook 'org-starter-load-all-files-in-path)
  (org-starter-def "~/Dropbox/org"
                   :files
                   ("agenda/inbox.org"                :agenda t   :key "i" :refile (:maxlevel . 2))
                   ("agenda/work.org"                 :agenda t   :key "w" :refile (:maxlevel . 2))
                   ("agenda/tech-debt.org"            :agenda t   :key "d" :refile (:maxlevel . 2))
                   ("agenda/personal.org"             :agenda t   :key "p" :refile (:maxlevel . 2))
                   ("agenda/books.org"                :agenda t   :key "b" :refile (:maxlevel . 2))
                   ("agenda/someday.org"              :agenda t   :key "s" :refile (:maxlevel . 2))
                   ("agenda/agenda.org"               :agenda t   :key "a" :refile (:maxlevel . 2))
                   ("agenda/note.org"                 :agenda t   :key "n" :refile (:maxlevel . 2))
                   ("daily/journal.org"               :agenda t   :key "j" :refile (:maxlevel . 2))
                   ("beancount/2022.beancount"        :agenda nil :key "c")
                   ("beancount/account.beancount"     :agenda nil :key "m")
                   ("beancount/credit.beancount"      :agenda nil :key "e")
                   ("beancount/insurance.beancount"   :agenda nil :key "u")
                   ("beancount/salary.beancount"      :agenda nil :key "l")
                   ("beancount/house.beancount"       :agenda nil :key "h")
                   )
  ;; hydra
  (require-package 'hydra)
  ;; 打开当前日期对应的 daily log 文件
  (defun open-today-journal-file ()
    "Open journal file for today."
    (interactive)
    (let* ((file-name (format-time-string "%Y-%m-%d.org"))
           (file-path (concat "~/Dropbox/org/daily/" file-name)))
      (if (file-exists-p file-path)
          (find-file file-path)
        (message "Journal file not found for today."))))

  (defun open-yesterday-journal-file ()
  "Open journal file for yesterday."
  (interactive)
  (let* ((yesterday-time (time-subtract (current-time) (days-to-time 1)))
         (file-name (format-time-string "%Y-%m-%d.org" yesterday-time))
         (file-path (concat "~/Dropbox/org/daily/" file-name)))
    (if (file-exists-p file-path)
        (find-file file-path)
      (message "Journal file not found for yesterday."))))

  (defhydra hydra-org-agenda-menu (:color blue)
    "
  [--> Org-agenda-menu <--]
  ^^^^------------------------------------------------
  _i_: inbox     _w_: work       _b_: books     _d_: tech-debt     _a_: agenda     _p_: personal
  _n_: note      _s_: someday    _j_: journal   _t_: today         _y_: yesterday
  [--> Beancount-menu <--]
  ^^^^------------------------------------------------
  _c_: 2022     _m_: account     _e_: credit     _u_: insurance     _l_: salary     _h_: house
  "
    ("i" org-starter-find-file:inbox)
    ("w" org-starter-find-file:work)
    ("d" org-starter-find-file:tech-debt)
    ("p" org-starter-find-file:personal)
    ("b" org-starter-find-file:books)
    ("n" org-starter-find-file:note)
    ("s" org-starter-find-file:someday)
    ("a" org-starter-find-file:agenda)
    ("j" org-starter-find-file:journal)
    ("t" (open-today-journal-file))
    ("y" (open-yesterday-journal-file))
    ("c" org-starter-find-file:2022)
    ("m" org-starter-find-file:account)
    ("e" org-starter-find-file:credit)
    ("u" org-starter-find-file:insurance)
    ("l" org-starter-find-file:salary)
    ("h" org-starter-find-file:house)
    ):bind("C-c e" . hydra-org-agenda-menu/body))
(provide 'init-quick-menus)
;;; init-quick-menus.el ends here

init-tree-sitter.el

其中 elisp 官方还未支持,需要自己编译后放入 tree-sitter-langs 下面。

git clone https://github.com/Wilfred/tree-sitter-elisp
gcc ./src/parser.c -fPIC -I./ --shared -o elisp.so
cp ./elisp.so ~/.tree-sitter-langs/bin

这里第二步的时候报错了

./src/parser.c:1:10: error: 'tree_sitter/parser.h' file not found with <angled> include; use "quotes" instead
#include <tree_sitter/parser.h>
         ^~~~~~~~~~~~~~~~~~~~~~
         "tree_sitter/parser.h"
1 error generated.

这里需要将 src/parser.c 头部按照提示修改一下。

- #include <tree_sitter/parser.h>
+ #include "tree_sitter/parser.h"

第三步,由于我是通过 MELPA 安装,路径为 .emacs.d/elpa-29.0/tree-sitter-langs-version/bin​,其中 version 为具体的版本号。

;;; init-tree-sitter.el --- Configure for tree sitter
;;; Commentary:

(require-package 'tree-sitter)
(require-package 'tree-sitter-langs)
(require 'tree-sitter)
(require 'tree-sitter-langs)

;;; Code:
(global-tree-sitter-mode)
(add-hook 'tree-sitter-after-on-hook #'tree-sitter-hl-mode)

;; Load the language definition for Rust, if it hasn't been loaded.
;; Return the language object.
(tree-sitter-require 'rust)
(tree-sitter-require 'python)
(tree-sitter-require 'javascript)

(provide 'init-tree-sitter)
;;; init-tree-sitter.el ends here

init-org-enhance.el

一些增强 org 功能的函数,可以方便使用。

;;; init-org-enhance.el --- Org-mode config -*- lexical-binding: t -*-
;;; Commentary:
;; =============================================================
;; ==============  Org-roam-backlinks-search  ==================
;; =============================================================
;; https://github.com/Vidianos-Giannitsis/Dotfiles/blob/master/emacs/.emacs.d/libs/zettelkasten.org#org-roam-backlinks-search
 (defcustom org-roam-backlinks-choices '("View Backlinks" "Go to Node" "Quit")
   "List of choices for `org-roam-backlinks-node-read'.")

 (defun org-roam-backlinks-query* (NODE)
   "Gets the backlinks of NODE with `org-roam-db-query'."
   (org-roam-db-query
          [:select [source dest]
                   :from links
                   :where (= dest $s1)
                   :and (= type "id")]
          (org-roam-node-id NODE)))

 (defun org-roam-backlinks-p (SOURCE NODE)
   "Predicate function that checks if NODE is a backlink of SOURCE."
   (let* ((source-id (org-roam-node-id SOURCE))
           (backlinks (org-roam-backlinks-query* SOURCE))
           (id (org-roam-node-id NODE))
           (id-list (list id source-id)))
     (member id-list backlinks)))

 (defun org-roam-backlinks-poi-or-moc-p (NODE)
   "Check if NODE has the tag POI or the tag MOC.  Return t if it does."
   (or (string-equal (car (org-roam-node-tags NODE)) "POI")
        (string-equal (car (org-roam-node-tags NODE)) "MOC")))

 (defun org-roam-backlinks--read-node-backlinks (source)
   "Runs `org-roam-node-read' on the backlinks of SOURCE.
 The predicate used as `org-roam-node-read''s filter-fn is
 `org-roam-backlinks-p'."
   (org-roam-node-read nil (apply-partially #'org-roam-backlinks-p source)))

 (defun org-roam-backlinks-node-read (node)
   "Read a NODE and run `org-roam-backlinks--read-node-backlinks'.
 Upon selecting a backlink, prompt the user for what to do with
 the backlink. The prompt is created with `completing-read' with
 valid options being everything in the list
 `org-roam-backlinks-choices'.

 If the user decides to view the selected node's backlinks, the
 function recursively runs itself with the selection as its
 argument. If they decide they want to go to the selected node,
 the function runs `find-file' and the file associated to that
 node. Lastly, if they choose to quit, the function exits
 silently."
   (let* ((backlink (org-roam-backlinks--read-node-backlinks node))
           (choice (completing-read "What to do with NODE: "
                                    org-roam-backlinks-choices)))
     (cond
      ((string-equal
         choice
         (car org-roam-backlinks-choices))
        (org-roam-backlinks-node-read backlink))
      ((string-equal
         choice
         (cadr org-roam-backlinks-choices))
        (find-file (org-roam-node-file backlink)))
      ((string-equal
         choice
         (caddr org-roam-backlinks-choices))))))

 (defun org-roam-backlinks-search ()
   "Select an `org-roam-node' and recursively search its backlinks.

 This function is a starter function for
 `org-roam-backlinks-node-read' which gets the initial node
 selection from `org-roam-node-list'. For more information about
 this function, check `org-roam-backlinks-node-read'."
   (interactive)
   (let ((node (org-roam-node-read)))
     (org-roam-backlinks-node-read node)))

 (defun org-roam-backlinks-search-from-moc-or-poi ()
   "`org-roam-backlinks-search' with an initial selection filter.

 Since nodes tagged as \"MOC\" or \"POI\" are the entry points to
 my personal zettelkasten, I have this helper function which is
 identical to `org-roam-backlinks-search' but filters initial
 selection to only those notes. That way, they initial selection
 has a point as it will be on a node that has a decent amount of
 backlinks."
   (interactive)
   (let ((node (org-roam-node-read nil #'org-roam-backlinks-poi-or-moc-p)))
     (org-roam-backlinks-node-read node)))

;; =============================================================
;; ===========  Dynamic org-agenda with org-roam  ==============
;; =============================================================
;; https://gist.github.com/d12frosted/a60e8ccb9aceba031af243dff0d19b2e
(defun vulpea-dynamic-p ()
  "Return non-nil if current buffer has any todo entry.
TODO entries marked as done are ignored, meaning the this
function returns nil if current buffer contains only completed
tasks."
  (seq-find                                 ; (3)
   (lambda (type)
     (eq type 'todo))
   (org-element-map                         ; (2)
       (org-element-parse-buffer 'headline) ; (1)
       'headline
     (lambda (h)
       (org-element-property :todo-type h)))))

(defun vulpea-dynamic-update-tag ()
    "Update dynamic tag in the current buffer."
    (when (and (not (active-minibuffer-window))
               (vulpea-buffer-p))
      (save-excursion
        (goto-char (point-min))
        (let* ((tags (vulpea-buffer-tags-get))
               (original-tags tags))
          (if (vulpea-dynamic-p)
              (setq tags (cons "dynamic" tags))
            (setq tags (remove "dynamic" tags)))

          ;; cleanup duplicates
          (setq tags (seq-uniq tags))

          ;; update tags if changed
          (when (or (seq-difference tags original-tags)
                    (seq-difference original-tags tags))
            (apply #'vulpea-buffer-tags-set tags))))))

(defun vulpea-buffer-p ()
  "Return non-nil if the currently visited buffer is a note."
  (and buffer-file-name
       (string-prefix-p
        (expand-file-name (file-name-as-directory org-roam-directory))
        (file-name-directory buffer-file-name))))

(defun vulpea-dynamic-files ()
    "Return a list of note files containing 'dynamic' tag." ;
    (seq-uniq
     (seq-map
      #'car
      (org-roam-db-query
       [:select [nodes:file]
        :from tags
        :left-join nodes
        :on (= tags:node-id nodes:id)
        :where (like tag (quote "%\"dynamic\"%"))]))))

(defun vulpea-agenda-files-update (&rest _)
  "Update the value of `org-agenda-files'."
  ;; (setq org-agenda-files (vulpea-dynamic-files)))
  (setq org-agenda-files (seq-uniq
                          (append
                           (vulpea-dynamic-files)
                           (file-expand-wildcards "~/Dropbox/org/agenda/*.org")))))

(add-hook 'find-file-hook #'vulpea-dynamic-update-tag)
(add-hook 'before-save-hook #'vulpea-dynamic-update-tag)

(advice-add 'org-agenda :before #'vulpea-agenda-files-update)
(advice-add 'org-todo-list :before #'vulpea-agenda-files-update)

;; functions borrowed from `vulpea' library
;; https://github.com/d12frosted/vulpea/blob/6a735c34f1f64e1f70da77989e9ce8da7864e5ff/vulpea-buffer.el

(defun vulpea-buffer-tags-get ()
  "Return filetags value in current buffer."
  (vulpea-buffer-prop-get-list "filetags" "[ :]"))

(defun vulpea-buffer-tags-set (&rest tags)
  "Set TAGS in current buffer.
If filetags value is already set, replace it."
  (if tags
      (vulpea-buffer-prop-set
       "filetags" (concat ":" (string-join tags ":") ":"))
    (vulpea-buffer-prop-remove "filetags")))

(defun vulpea-buffer-tags-add (tag)
  "Add a TAG to filetags in current buffer."
  (let* ((tags (vulpea-buffer-tags-get))
         (tags (append tags (list tag))))
    (apply #'vulpea-buffer-tags-set tags)))

(defun vulpea-buffer-tags-remove (tag)
  "Remove a TAG from filetags in current buffer."
  (let* ((tags (vulpea-buffer-tags-get))
         (tags (delete tag tags)))
    (apply #'vulpea-buffer-tags-set tags)))

(defun vulpea-buffer-prop-set (name value)
  "Set a file property called NAME to VALUE in buffer file.
If the property is already set, replace its value."
  (setq name (downcase name))
  (org-with-point-at 1
    (let ((case-fold-search t))
      (if (re-search-forward (concat "^#\\+" name ":\\(.*\\)")
                             (point-max) t)
          (replace-match (concat "#+" name ": " value) 'fixedcase)
        (while (and (not (eobp))
                    (looking-at "^[#:]"))
          (if (save-excursion (end-of-line) (eobp))
              (progn
                (end-of-line)
                (insert "\n"))
            (forward-line)
            (beginning-of-line)))
        (insert "#+" name ": " value "\n")))))

(defun vulpea-buffer-prop-set-list (name values &optional separators)
  "Set a file property called NAME to VALUES in current buffer.
VALUES are quoted and combined into single string using
`combine-and-quote-strings'.
If SEPARATORS is non-nil, it should be a regular expression
matching text that separates, but is not part of, the substrings.
If nil it defaults to `split-string-default-separators', normally
\"[ \f\t\n\r\v]+\", and OMIT-NULLS is forced to t.
If the property is already set, replace its value."
  (vulpea-buffer-prop-set
   name (combine-and-quote-strings values separators)))

(defun vulpea-buffer-prop-get (name)
  "Get a buffer property called NAME as a string."
  (org-with-point-at 1
    (when (re-search-forward (concat "^#\\+" name ": \\(.*\\)")
                             (point-max) t)
      (buffer-substring-no-properties
       (match-beginning 1)
       (match-end 1)))))

(defun vulpea-buffer-prop-get-list (name &optional separators)
  "Get a buffer property NAME as a list using SEPARATORS.
If SEPARATORS is non-nil, it should be a regular expression
matching text that separates, but is not part of, the substrings.
If nil it defaults to `split-string-default-separators', normally
\"[ \f\t\n\r\v]+\", and OMIT-NULLS is forced to t."
  (let ((value (vulpea-buffer-prop-get name)))
    (when (and value (not (string-empty-p value)))
      (split-string-and-unquote value separators))))

(defun vulpea-buffer-prop-remove (name)
  "Remove a buffer property called NAME."
  (org-with-point-at 1
    (when (re-search-forward (concat "\\(^#\\+" name ":.*\n?\\)")
                             (point-max) t)
      (replace-match ""))))

;; =============================================================
;; ======================  Archiving  ==========================
;; =============================================================

;; (setq org-archive-mark-done nil)
;; (setq org-archive-location "%s_archive::* Archive")

;; https://gist.github.com/kepi/2f4acc3cc93403c75fbba5684c5d852d
;; org-archive-subtree-hierarchical.el
;;
;; version 0.2
;; modified from https://lists.gnu.org/archive/html/emacs-orgmode/2014-08/msg00109.html
;; modified from https://stackoverflow.com/a/35475878/259187

;; In orgmode
;; * A
;; ** AA
;; *** AAA
;; ** AB
;; *** ABA
;; Archiving AA will remove the subtree from the original file and create
;; it like that in archive target:

;; * AA
;; ** AAA

;; And this give you
;; * A
;; ** AA
;; *** AAA
;;
;; Install file to your include path and include in your init file with:
;;
;;  (require 'org-archive-subtree-hierarchical)
;;  (setq org-archive-default-command 'org-archive-subtree-hierarchical)
;;
(provide 'org-archive-subtree-hierarchical)
(setq org-archive-default-command 'org-archive-subtree-hierarchical)
(require 'org-archive)

(defun org-archive-subtree-hierarchical--line-content-as-string ()
  "Returns the content of the current line as a string"
  (save-excursion
    (beginning-of-line)
    (buffer-substring-no-properties
     (line-beginning-position) (line-end-position))))

(defun org-archive-subtree-hierarchical--org-child-list ()
  "This function returns all children of a heading as a list. "
  (interactive)
  (save-excursion
    ;; this only works with org-version > 8.0, since in previous
    ;; org-mode versions the function (org-outline-level) returns
    ;; gargabe when the point is not on a heading.
    (if (= (org-outline-level) 0)
        (outline-next-visible-heading 1)
      (org-goto-first-child))
    (let ((child-list (list (org-archive-subtree-hierarchical--line-content-as-string))))
      (while (org-goto-sibling)
        (setq child-list (cons (org-archive-subtree-hierarchical--line-content-as-string) child-list)))
      child-list)))

(defun org-archive-subtree-hierarchical--org-struct-subtree ()
  "This function returns the tree structure in which a subtree
belongs as a list."
  (interactive)
  (let ((archive-tree nil))
    (save-excursion
      (while (org-up-heading-safe)
        (let ((heading
               (buffer-substring-no-properties
                (line-beginning-position) (line-end-position))))
          (if (eq archive-tree nil)
              (setq archive-tree (list heading))
            (setq archive-tree (cons heading archive-tree))))))
    archive-tree))

(defun org-archive-subtree-hierarchical ()
  "This function archives a subtree hierarchical"
  (interactive)
  (let ((org-tree (org-archive-subtree-hierarchical--org-struct-subtree))
        (this-buffer (current-buffer))
        (file (abbreviate-file-name
               (or (buffer-file-name (buffer-base-buffer))
                   (error "No file associated to buffer")))))
    (save-excursion
      (setq location org-archive-location
            afile (car (org-archive--compute-location
                                   (or (org-entry-get nil "ARCHIVE" 'inherit) location)))
            ;; heading (org-extract-archive-heading location)
            infile-p (equal file (abbreviate-file-name (or afile ""))))
      (unless afile
        (error "Invalid `org-archive-location'"))
      (if (> (length afile) 0)
          (setq newfile-p (not (file-exists-p afile))
                visiting (find-buffer-visiting afile)
                buffer (or visiting (find-file-noselect afile)))
        (setq buffer (current-buffer)))
      (unless buffer
        (error "Cannot access file \"%s\"" afile))
      (org-cut-subtree)
      (set-buffer buffer)
      (org-mode)
      (goto-char (point-min))
      (while (not (equal org-tree nil))
        (let ((child-list (org-archive-subtree-hierarchical--org-child-list)))
          (if (member (car org-tree) child-list)
              (progn
                (search-forward (car org-tree) nil t)
                (setq org-tree (cdr org-tree)))
            (progn
              (goto-char (point-max))
              (newline)
              (org-insert-struct org-tree)
              (setq org-tree nil)))))
      (newline)
      (org-yank)
      (when (not (eq this-buffer buffer))
        (save-buffer))
      (message "Subtree archived %s"
               (concat "in file: " (abbreviate-file-name afile))))))

(defun org-insert-struct (struct)
  "TODO"
  (interactive)
  (when struct
    (insert (car struct))
    (newline)
    (org-insert-struct (cdr struct))))

(defun org-archive-subtree ()
  (org-archive-subtree-hierarchical)
  )

;; =============================================================
;; ==============  Hierachy for title nodes  ===================
;; =============================================================
;; Codes blow are used to general a hierachy for title nodes that under a file
(cl-defmethod org-roam-node-doom-filetitle ((node org-roam-node))
  "Return the value of \"#+title:\" (if any) from file that NODE resides in.
If there's no file-level title in the file, return empty string."
  (or (if (= (org-roam-node-level node) 0)
          (org-roam-node-title node)
        (org-roam-get-keyword "TITLE" (org-roam-node-file node)))
      ""))
(cl-defmethod org-roam-node-doom-hierarchy ((node org-roam-node))
  "Return hierarchy for NODE, constructed of its file title, OLP and direct title.
  If some elements are missing, they will be stripped out."
  (let ((title     (org-roam-node-title node))
        (olp       (org-roam-node-olp   node))
        (level     (org-roam-node-level node))
        (filetitle (org-roam-node-doom-filetitle node))
        (separator (propertize " > " 'face 'shadow)))
    (cl-case level
      ;; node is a top-level file
      (0 filetitle)
      ;; node is a level 1 heading
      (1 (concat (propertize filetitle 'face '(shadow italic))
                 separator title))
      ;; node is a heading with an arbitrary outline path
      (t (concat (propertize filetitle 'face '(shadow italic))
                 separator (propertize (string-join olp " > ") 'face '(shadow italic))
                 separator title)))))

(setq org-roam-node-display-template (concat "${type:10} ${doom-hierarchy:120} " (propertize "${tags:*}" 'face 'org-tag)))
;; =============================================================
;; ========================  iscroll  ==========================
;; =============================================================
(require-package 'iscroll)
(add-hook 'org-mode-hook (lambda () (iscroll-mode 1)))
;; =============================================================
;; ===================  dwim-shell-command  ====================
;; =============================================================
(require-package 'dwim-shell-command)
(require 'dwim-shell-command)
(defun dwim-shell-commands-macos-reveal-in-finder ()
  "Reveal selected files in macOS Finder."
  (interactive)
  (dwim-shell-command-on-marked-files
   "Reveal in Finder"
   "import AppKit
    NSWorkspace.shared.activateFileViewerSelecting([\"<<*>>\"].map{URL(fileURLWithPath:$0)})"
   :join-separator ", "
   :silent-success t
   :shell-pipe "swift -"))
(provide 'init-org-enhance)
;;; init-org-enhance.el ends here

telega-bridge-bot.el

用来改善 Matrix 机器人在 TG 中转发的消息展示,包括头像、用户名等。具体源码参见 vendor/telega-bridge-bot.el · BlindingDark/BEmacs - Gitee.com

init-org-transclusion.el

过滤的条件的具体细节见 Org-transclusion User Manual

;;; init-org-transclusion.el  --- Custom configuration
;;; Commentary
(require-package 'org-transclusion)
(use-package org-transclusion
              :after org
              :bind(("C-c n t" . org-transclusion-mode)))

(defun trival/org-transclusion-select-source (beg end)
  "Send transclusion information to kill-ring. See
    https://org-roam.discourse.group/t/alpha-org-transclusion/830/122"
  (interactive "r")
  (let ((lbeg (line-number-at-pos beg))
        (lend (line-number-at-pos end))
        (filename (concat "~/"(string-remove-prefix (file-truename "~/") (buffer-file-name)))))
    (with-temp-buffer
        (progn
          (insert "#+transclude: [[file:")
          (insert filename)
          (insert (format "]] :lines %d-%d" lbeg lend))
          (clipboard-kill-region (point-min) (point-max))))
    (message "A transcluded link has been sent to your kill-ring.")))
(defun my/org-insert-link-transclusion (&optional COMPLETE-FILE LINK-LOCATION DESCRIPTION)
  (interactive "P")
  (org-insert-link COMPLETE-FILE LINK-LOCATION DESCRIPTION)
  (org-transclusion-make-from-link))
(provide 'init-org-transclusion)
;;; init-org-transclusion.el ends here

Export

对于只需要导出某个 header 下的内容的需求,只需要在 header 上加上 :export: 即可。

Markdown file

brew install pandoc

这里用的是 ox-pandoc,需要先 M-x package-install <RET> ox-pandoc <RET> 进行安装,​M-x org-pandoc-export-as-gfm 算是我找到最符合 MarkDown 语法的转换了,Emacs 自己的转换会将 Table 转换成 HTML 标签的格式。

Hugo

在文件头部添加

#+HUGO_BASE_DIR: ~/Dropbox/hugo/
#+HUGO_SECTION: posts/main
#+HUGO_WEIGHT: auto
#+HUGO_AUTO_SET_LASTMOD: t

需要导出为 <mark></mark>1

#+begin_mark
marked text
#+end_mark

需要导出为块级 <mark></mark> 时,也就是前后的空格不进行 trim。

#+header: :trim-pre nil :trim-post nil
#+begin_mark
marked text
#+end_mark

Latex

Dependency

需要安装 texlive

brew install texlive

Latex 语法

Latex 语法

PDF 导出报错

  • Unicode character 考 (U+8003) not set up for use with LaTeX. 是因为 Latex 本身不支持中文。

中文 PDF 导出设置

  1. 使用 ElegantPaper,需要将 elegantpaper.cls 文件放在 org 目录下。
  2. 安装依赖

代码块需要用到 minted​,需要用到 pygments 作为依赖。

brew install pygments
  1. 导出文件头部增加
#+LATEX_COMPILER: xelatex
#+LATEX_CLASS: elegantpaper
#+OPTIONS: prop:t

Links to this note