~cytrogen/.emacs.d

40b26f90a0a33e58fafcaa386388cc51a0ab64cc — Cytrogen a month ago 53c8cc8
feat(org): 添加动态 Capture/Refile 系统和阅读列表 Agenda

动态结构系统: 由 org-structure.org 数据驱动生成 Capture 模板
和 Refile 目标,支持 datetree、自定义模板、文件覆盖
- 一级标题定义分类(FILE + KEY 属性)
- 二级标题定义子项(HEADLINE + TEMPLATE + KEY)

快速 Refile: C-c w 两步交互式 Refile(先选分类再选目标)

阅读列表: 自定义 Agenda 视图(r 前缀),支持想读/在读/已读
三种状态,Agenda 操作 S 开始阅读、F 完成阅读

其他: CJK 内联标记支持、源代码块原生 TAB、RET 直接跟踪链接
+ Babel shell/elisp 支持 + 数学公式 pretty entities
1 files changed, 161 insertions(+), 2 deletions(-)

M config/pkg-org.el
M config/pkg-org.el => config/pkg-org.el +161 -2
@@ 1,6 1,6 @@
;;; pkg-org.el --- Org-mode configuration and dynamic system -*- lexical-binding: t -*-

;; Copyright (C) 2024 Cytrogen
;; Copyright (C) 2026 Cytrogen

;; This file contains:
;; - Org directory setup


@@ 232,12 232,56 @@ Format: ((key label file ((subkey sublabel headline template override-file) ...)
    (define-key org-agenda-mode-map (kbd "S") #'my/reading-start-book)
    (define-key org-agenda-mode-map (kbd "F") #'my/reading-finish-book))

  ;; --- 课程笔记 Capture ---
  (defvar my/class-notes-date nil "临时存储日期,供模板使用")

  (defun my/get-class-subjects ()
    "获取 reading.org 中 课程笔记 下的所有科目。"
    (let ((reading-file (expand-file-name "reading.org" org-directory))
          (subjects '()))
      (when (file-exists-p reading-file)
        (with-temp-buffer
          (insert-file-contents reading-file)
          (goto-char (point-min))
          (when (re-search-forward "^\\* 课程笔记" nil t)
            (let ((end (save-excursion
                         (or (re-search-forward "^\\* " nil t) (point-max)))))
              (while (re-search-forward "^\\*\\* \\(.+\\)" end t)
                (push (match-string-no-properties 1) subjects))))))
      (nreverse subjects)))

  (defun my/class-notes-goto-subject ()
    "提示选择科目和日期,定位到科目标题下。"
    (let* ((subjects (my/get-class-subjects))
           (subject (completing-read "科目: " subjects nil nil))
           (date (read-string "日期: " (format-time-string "%Y-%m-%d"))))
      (setq my/class-notes-date date)
      (goto-char (point-min))
      (if (re-search-forward "^\\* 课程笔记" nil t)
          (let ((subtree-end (save-excursion (org-end-of-subtree t) (point))))
            (if (re-search-forward (format "^\\*\\* %s$" (regexp-quote subject)) subtree-end t)
                (beginning-of-line)
              ;; 科目不存在,在子树末尾创建
              (goto-char subtree-end)
              (insert "\n** " subject)
              (beginning-of-line)))
        ;; 课程笔记也不存在
        (goto-char (point-max))
        (insert "\n* 课程笔记\n** " subject)
        (beginning-of-line))))

  ;; --- Refile Configuration ---
  ;; 强化 Refile 功能,允许将条目移动到项目或分类的具体标题下
  (setq org-refile-use-outline-path 'file)
  (setq org-outline-path-complete-in-steps nil)
  (setq org-refile-allow-creating-parent-nodes 'confirm)

  ;; --- 数学公式显示配置 ---
  ;; 启用 pretty entities 显示上标/下标
  (setq org-pretty-entities t)
  ;; 可选:只在需要时显示(光标不在实体上时)
  (setq org-pretty-entities-include-sub-superscripts t)

  ;; --- Source Block Editing Configuration ---
  ;; 让 TAB 在源代码块中按语言原生方式工作
  (setq org-src-tab-acts-natively t)


@@ 248,7 292,21 @@ Format: ((key label file ((subkey sublabel headline template override-file) ...)
  ;; 在当前窗口编辑源代码块(避免分割窗口)
  (setq org-src-window-setup 'current-window)
  ;; 禁用返回编辑 buffer 时的确认提示
  (setq org-src-ask-before-returning-to-edit-buffer nil))
  (setq org-src-ask-before-returning-to-edit-buffer nil)

  ;; --- Link Configuration ---
  ;; 按 RET 时直接打开链接
  (setq org-return-follows-link t)

  ;; --- Org-Babel Configuration ---
  ;; 启用语言支持,实现类似 Jupyter Notebook 的体验
  (org-babel-do-load-languages
   'org-babel-load-languages
   '((shell . t)        ; 用于编译和运行命令
     (emacs-lisp . t))) ; 默认支持

  ;; 执行代码块时不询问确认(方便快速执行)
  (setq org-confirm-babel-evaluate nil))

;; Custom Org Functions
;; Helper functions for enhanced workflow


@@ 308,6 366,17 @@ Format: ((key label file ((subkey sublabel headline template override-file) ...)
            (push (list combined-key label type capture-target template) templates)))))

    (setq org-capture-templates (reverse templates))
    ;; 替换课程笔记模板为使用 file+function
    (setq org-capture-templates
          (mapcar (lambda (tpl)
                    (if (equal (car tpl) "Rc")
                        `("Rc" "课程笔记" entry
                          (file+function ,(expand-file-name "reading.org" org-directory)
                                         my/class-notes-goto-subject)
                          "*** %(symbol-value 'my/class-notes-date)\n%?"
                          :empty-lines 1)
                      tpl))
                  org-capture-templates))
    (message "Org capture templates updated.")))

(defun my/quick-refile ()


@@ 383,5 452,95 @@ Step 2: Choose Sub Category (Context) using completing-read.
(with-eval-after-load 'org
  (my/apply-org-structure))

;; 中文行内标记支持
;; 允许中文字符紧贴 org 强调标记符(*粗体*、/斜体/ 等),无需额外空格
(with-eval-after-load 'org
  (setcar org-emphasis-regexp-components
          "-[:space:]('\"{\x200B[:nonascii:]")
  (setcar (nthcdr 1 org-emphasis-regexp-components)
          "-[:space:].,:!?;'\")}\\[\x200B[:nonascii:]")
  (org-set-emph-re 'org-emphasis-regexp-components org-emphasis-regexp-components)
  ;; 使用 _{xxx}_ 语法时,防止下划线被解释为下标
  (setq org-use-sub-superscripts '{})
  (setq org-export-with-sub-superscripts '{}))

;;; ---- 下划线语法迁移 ----

(defun my/migrate-underline-syntax (&optional dry-run)
  "Migrate _xxx_ to _{xxx}_ in all org files under `org-directory'.
With prefix argument (C-u), perform a DRY-RUN showing changes without modifying files."
  (interactive "P")
  (let ((files (directory-files-recursively org-directory "\\.org$"))
        (total-replacements 0)
        (modified-files '())
        ;; Regex matching Org emphasis _xxx_ pattern
        ;; Matches _ + non-space/non-underscore + optional middle + non-space/non-underscore + _
        ;; but NOT already in _{xxx}_ form
        (underline-re "_\\([^ _\n]\\(?:[^_\n]*[^ _\n]\\)?\\)_"))
    (dolist (file files)
      (with-temp-buffer
        (insert-file-contents file)
        (let ((file-replacements 0)
              (regions-to-skip '()))
          ;; Collect regions to skip: src blocks, example blocks, property drawers
          (goto-char (point-min))
          (while (re-search-forward
                  "^[ \t]*#\\+BEGIN_\\(SRC\\|EXAMPLE\\)\\b.*\n" nil t)
            (let ((beg (match-beginning 0)))
              (when (re-search-forward
                     (format "^[ \t]*#\\+END_%s" (match-string 1)) nil t)
                (push (cons beg (line-end-position)) regions-to-skip))))
          (goto-char (point-min))
          (while (re-search-forward "^[ \t]*:PROPERTIES:" nil t)
            (let ((beg (match-beginning 0)))
              (when (re-search-forward "^[ \t]*:END:" nil t)
                (push (cons beg (line-end-position)) regions-to-skip))))
          ;; Collect inline code =...= and ~...~
          (goto-char (point-min))
          (while (re-search-forward "\\(?:=\\([^=\n]+\\)=\\)\\|\\(?:~\\([^~\n]+\\)~\\)" nil t)
            (push (cons (match-beginning 0) (match-end 0)) regions-to-skip))
          ;; Collect links [[...]]
          (goto-char (point-min))
          (while (re-search-forward "\\[\\[\\([^]]*\\)\\]\\(?:\\[\\([^]]*\\)\\]\\)?\\]" nil t)
            (push (cons (match-beginning 0) (match-end 0)) regions-to-skip))
          ;; Collect #+KEYWORD: lines
          (goto-char (point-min))
          (while (re-search-forward "^[ \t]*#\\+[A-Z_]+:.*$" nil t)
            (push (cons (match-beginning 0) (match-end 0)) regions-to-skip))
          ;; Now do replacements, skipping protected regions
          (goto-char (point-min))
          (while (re-search-forward underline-re nil t)
            (let ((pos (match-beginning 0))
                  (skip nil))
              ;; Check if inside a skip region
              (dolist (region regions-to-skip)
                (when (and (>= pos (car region)) (<= pos (cdr region)))
                  (setq skip t)))
              ;; Skip if already in _{...}_ form (preceded by _ and { )
              (when (and (not skip)
                         (> pos 0)
                         (eq (char-before (match-beginning 0)) ?{))
                (setq skip t))
              (unless skip
                (cl-incf file-replacements)
                (unless dry-run
                  (replace-match "_{\\1}_")))))
          (when (> file-replacements 0)
            (cl-incf total-replacements file-replacements)
            (push (cons file file-replacements) modified-files)
            (unless dry-run
              (write-region (point-min) (point-max) file))))))
    ;; Report results
    (if (= total-replacements 0)
        (message "No _xxx_ patterns found to migrate.")
      (with-current-buffer (get-buffer-create "*underline-migration*")
        (erase-buffer)
        (insert (format "%s — %d replacements in %d files:\n\n"
                        (if dry-run "DRY RUN" "MIGRATED")
                        total-replacements (length modified-files)))
        (dolist (entry (nreverse modified-files))
          (insert (format "  %s: %d\n" (car entry) (cdr entry))))
        (display-buffer (current-buffer))))))

(provide 'pkg-org)
;;; pkg-org.el ends here
\ No newline at end of file