~cytrogen/.emacs.d

ref: 504e0080a3f94c9824ac305b1a24b30c7d1fb051 .emacs.d/config/pkg-org.el -rw-r--r-- 16.8 KiB
504e0080 — HallowDem feat: 添加博客图片插入功能 3 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
;;; pkg-org.el --- Org-mode configuration and dynamic system -*- lexical-binding: t -*-

;; Copyright (C) 2024 Cytrogen

;; This file contains:
;; - Org directory setup
;; - Dynamic structure system for Capture and Refile
;; - Agenda and workflow configurations
;; - Org-mode customizations

;;; Commentary:

;; Complete Org-mode configuration including the custom dynamic structure
;; system that provides flexible capture templates and refile targets.

;;; Code:

;; Org Directory Configuration
;; Dynamic path setup with user interaction
(defvar my/org-path-config-file (concat user-emacs-directory ".org-path")
  "File to store the user's Org directory path.")

(defun my/setup-org-directory ()
  "Load Org directory from config file or prompt user to select one."
  (let ((path nil))
    ;; 1. Try to read from file
    (when (file-exists-p my/org-path-config-file)
      (with-temp-buffer
        (insert-file-contents my/org-path-config-file)
        (setq path (string-trim (buffer-string)))))
    
    ;; 2. Validate path, if invalid/missing, prompt user
    (unless (and path (file-directory-p path))
      (setq path (read-directory-name "请选择您的 Org 笔记根目录 (Please select Org root): " "D:/"))
      (unless (file-directory-p path)
        (make-directory path t))
      ;; Save for next time
      (with-temp-file my/org-path-config-file
        (insert path)))
    
    ;; 3. Apply setting
    (setq org-directory (file-name-as-directory path))
    (setq org-default-notes-file (concat org-directory "inbox.org"))
    (message "Org Directory loaded: %s" org-directory)))

;; Execute setup immediately
(my/setup-org-directory)

;; Set agenda files to include all org files in the directory
(setq org-agenda-files 
      (append (list org-directory)
              (when (file-exists-p org-directory)
                (directory-files org-directory t "\\.org$"))))

;; Ensure Org directories exist immediately to prevent crashes during config load
(let ((notes-dir (concat org-directory "notes/")))
  (unless (file-exists-p notes-dir)
    (make-directory notes-dir t)
    (message "Created missing directory: %s" notes-dir)))

;; Dynamic Structure System
;; Flexible capture and refile system
(defvar my/org-structure-file (expand-file-name "org-structure.org" user-emacs-directory)
  "Path to the Org file defining capture templates and refile targets.")

(defvar my/parsed-org-structure nil
  "Cached structure parsed from my/org-structure-file.")

(defvar my/org-structure-loading nil
  "Flag to prevent infinite loading loops.")

(defun my/parse-org-properties-at-point ()
  "Parse properties for the current headline without using org-entry-properties."
  (save-excursion
    (let ((props '())
          (case-fold-search t))
      ;; Look for properties drawer after the headline
      (when (re-search-forward ":PROPERTIES:" (line-end-position 10) t)
        (let ((props-start (point)))
          (when (re-search-forward "^[ \t]*:END:" nil t)
            (let ((props-end (match-beginning 0)))
              (goto-char props-start)
              (while (re-search-forward "^[ \t]*:\\([^:]+\\):[ \t]*\\(.*\\)$" props-end t)
                (push (cons (match-string 1) (match-string 2)) props))))))
      props)))

(defun my/load-org-structure ()
  "Parse the structure file into a list of categories.
Format: ((key label file ((subkey sublabel headline template override-file) ...)) ...)"
  (interactive)
  (when my/org-structure-loading
    (message "Warning: Structure loading already in progress, skipping...")
    (cl-return-from my/load-org-structure nil))
  
  (if (not (file-exists-p my/org-structure-file))
      (message "Org structure file not found: %s" my/org-structure-file)
    (setq my/org-structure-loading t)
    (unwind-protect
        (with-temp-buffer
          (let ((coding-system-for-read 'utf-8))
            (insert-file-contents my/org-structure-file))
          (let ((result '())
                (current-main nil))
            (goto-char (point-min))
            (while (re-search-forward "^\\(\\*+\\) \\(.*\\)" nil t)
              (let* ((level (length (match-string 1)))
                     (title (match-string 2))
                     (props (my/parse-org-properties-at-point)))
                (cond
                 ((= level 1)
                  (let ((key (cdr (assoc "KEY" props)))
                        (file (cdr (assoc "FILE" props))))
                    (if (and key file (not (string-empty-p key)) (not (string-empty-p file)))
                        (progn
                          (setq current-main (list key title file '()))
                          (push current-main result))
                      ;; Debug: 跳过无效的一级标题
                      (message "Skipping invalid level-1 heading: %s (missing KEY or FILE)" title))))
                 ((and (= level 2) current-main)
                  (let* ((key (cdr (assoc "KEY" props)))
                         (headline (cdr (assoc "HEADLINE" props)))
                         (raw-template (cdr (assoc "TEMPLATE" props)))
                         ;; Convert literal \n to actual newlines
                         (template (when raw-template
                                     (replace-regexp-in-string (regexp-quote "\\n") "\n" raw-template)))
                         (file-override (cdr (assoc "FILE" props)))
                         (type-str (cdr (assoc "TYPE" props)))
                         (type (if (and type-str (not (string-empty-p type-str)))
                                   (intern type-str)
                                 'entry))
                         (sub-list (nth 3 current-main)))
                    (when key
                      (let ((entry (list key title headline template file-override type)))
                        (setf (nth 3 current-main) (append sub-list (list entry))))))))))
            (setq my/parsed-org-structure (reverse result))
            (message "Org structure loaded with %d main categories." (length result))))
      (setq my/org-structure-loading nil))))

;; Org Workflow Configuration
;; Agenda, refile, and capture setup
(with-eval-after-load 'org
  ;; --- Custom Agenda Views ---
  ;; 将"想读"的书转为"正在读"(创建书籍笔记并删除原条目)
  (defun my/reading-start-book ()
    "Start reading a book: create a note entry and remove from wishlist."
    (interactive)
    (org-agenda-check-type t 'agenda 'tags)
    (let* ((marker (or (org-get-at-bol 'org-marker)
                       (org-agenda-error)))
           (buffer (marker-buffer marker))
           (pos (marker-position marker))
           book-name)
      ;; 获取书名
      (with-current-buffer buffer
        (goto-char pos)
        (setq book-name (org-get-heading t t t t)))
      ;; 确认操作
      (when (y-or-n-p (format "开始阅读《%s》?" book-name))
        ;; 删除原条目
        (org-agenda-kill)
        ;; 创建书籍笔记
        (let ((org-capture-templates
               `(("b" "书籍笔记" entry
                  (file+headline ,(expand-file-name "reading.org" org-directory) "书籍笔记")
                  ,(format "** 《%s》笔记 %%^{序号}: %%^{标题}\n%%U\n\n%%?" book-name)))))
          (org-capture nil "b")))))

  ;; 将"正在读"的书标记为"读完"(创建读后感)
  (defun my/reading-finish-book ()
    "Finish reading a book: create a review entry."
    (interactive)
    (org-agenda-check-type t 'agenda 'tags)
    (let* ((marker (or (org-get-at-bol 'org-marker)
                       (org-agenda-error)))
           (buffer (marker-buffer marker))
           (pos (marker-position marker))
           book-name)
      ;; 从标题中提取书名(格式:《书名》笔记 X: 标题)
      (with-current-buffer buffer
        (goto-char pos)
        (let ((heading (org-get-heading t t t t)))
          (if (string-match "《\\([^》]+\\)》" heading)
              (setq book-name (match-string 1 heading))
            (setq book-name (read-string "书名: ")))))
      ;; 创建读后感
      (let ((org-capture-templates
             `(("d" "读后感" entry
                (file+headline ,(expand-file-name "reading.org" org-directory) "读完")
                ,(format "** 《%s》读后感\n:PROPERTIES:\n:FINISHED: %%U\n:END:\n\n%%?" book-name)))))
        (org-capture nil "d"))))

  ;; 辅助函数:跳过不在指定父标题下的条目
  (defun my/org-agenda-skip-unless-parent (parent-headline)
    "Skip entry unless its parent headline matches PARENT-HEADLINE."
    (let ((dominated nil))
      (save-excursion
        (while (and (not dominated) (org-up-heading-safe))
          (when (string= (org-get-heading t t t t) parent-headline)
            (setq dominated t))))
      (if dominated
          nil  ; 不跳过
        (save-excursion (org-end-of-subtree t)))))  ; 跳过

  (setq org-agenda-custom-commands
        '(;; Reading lists
          ("r" . "Reading (书单)")
          ("rw" "想读的书" tags "LEVEL=2"
           ((org-agenda-files (list (expand-file-name "reading.org" org-directory)))
            (org-agenda-overriding-header "想读的书")
            (org-agenda-prefix-format "  ")
            (org-agenda-skip-function '(my/org-agenda-skip-unless-parent "想读"))))
          ("rb" "正在读 (书籍笔记)" tags "LEVEL=2"
           ((org-agenda-files (list (expand-file-name "reading.org" org-directory)))
            (org-agenda-overriding-header "正在读的书")
            (org-agenda-prefix-format "  ")
            (org-agenda-skip-function '(my/org-agenda-skip-unless-parent "书籍笔记"))))
          ("rd" "读完的书" tags "LEVEL=2"
           ((org-agenda-files (list (expand-file-name "reading.org" org-directory)))
            (org-agenda-overriding-header "读完的书")
            (org-agenda-prefix-format "  ")
            (org-agenda-skip-function '(my/org-agenda-skip-unless-parent "读完"))))
          ;; Inbox items
          ("i" . "Inbox")
          ("is" "分享项目" tags "LEVEL=2"
           ((org-agenda-files (list (expand-file-name "inbox.org" org-directory)))
            (org-agenda-overriding-header "分享的项目/工具")
            (org-agenda-prefix-format "  ")
            (org-agenda-skip-function '(my/org-agenda-skip-unless-parent "分享"))))))

  ;; Agenda 快捷键:书单操作
  (with-eval-after-load 'org-agenda
    (define-key org-agenda-mode-map (kbd "S") #'my/reading-start-book)
    (define-key org-agenda-mode-map (kbd "F") #'my/reading-finish-book))

  ;; --- 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)

  ;; --- Source Block Editing Configuration ---
  ;; 让 TAB 在源代码块中按语言原生方式工作
  (setq org-src-tab-acts-natively t)
  ;; 保留源代码块的缩进,不自动调整
  (setq org-src-preserve-indentation t)
  ;; 源代码块内容不额外缩进(相对于 #+BEGIN_SRC)
  (setq org-edit-src-content-indentation 0)
  ;; 在当前窗口编辑源代码块(避免分割窗口)
  (setq org-src-window-setup 'current-window)
  ;; 禁用返回编辑 buffer 时的确认提示
  (setq org-src-ask-before-returning-to-edit-buffer nil))

;; Custom Org Functions
;; Helper functions for enhanced workflow
(defun my/open-org-file ()
  "Open an org file from the org directory."
  (interactive)
  (let ((file (read-file-name "Open Org file: " org-directory nil t)))
    (find-file file)))

(defun my/reload-org-structure ()
  "Force reload the org structure file."
  (interactive)
  (setq my/parsed-org-structure nil)
  (my/load-org-structure))

(defun my/apply-org-structure ()
  "Generate capture templates from the parsed structure."
  (interactive)
  (unless my/parsed-org-structure
    (my/load-org-structure))

  (let ((templates '()))
    (dolist (main my/parsed-org-structure)
      (let ((main-key (nth 0 main))
            (main-label (nth 1 main))
            (main-file (nth 2 main))
            (sub-cats (nth 3 main)))

        ;; 只添加有效的一级菜单项(有子分类的)
        (when sub-cats
          (push (list main-key main-label) templates))

        ;; 添加二级模板项(使用组合键)
        (dolist (sub sub-cats)
          (let* ((sub-key (nth 0 sub))
                 (combined-key (concat main-key sub-key))
                 (label (nth 1 sub))
                 (headline (nth 2 sub))
                 (template (nth 3 sub))
                 (file-ov (nth 4 sub))
                 (type (or (nth 5 sub) 'entry))
                 (target-file (expand-file-name (or file-ov main-file) org-directory))
                 (capture-target
                  (cond
                   ((and headline (string= headline "datetree"))
                    (list 'file+olp+datetree target-file))
                   ((and headline (not (string-empty-p headline)))
                    (list 'file+headline target-file headline))
                   (t
                    (list 'file target-file)))))

            (unless template
              (setq template (if (eq type 'item)
                                 "- %?"
                               "* TODO %?\n  %U")))

            (push (list combined-key label type capture-target template) templates)))))

    (setq org-capture-templates (reverse templates))
    (message "Org capture templates updated.")))

(defun my/quick-refile ()
  "Interactive refile using the dynamic org structure with improved interface.
Step 1: Choose Main Category (File) using completing-read.
Step 2: Choose Sub Category (Context) using completing-read.
   - If Sub Category has a specific HEADLINE, move there.
   - If HEADLINE is empty/nil, perform interactive org-refile in that file."
  (interactive)
  (unless my/parsed-org-structure
    (my/load-org-structure))
  
  ;; Debug: 显示解析结果
  (message "Parsed structure has %d categories" (length my/parsed-org-structure))
  (if (= 0 (length my/parsed-org-structure))
      (message "No categories found! Structure file might have parsing issues.")
    
    (progn
      (unless (org-region-active-p)
        (org-back-to-heading t))
  
  ;; Step 1: Select Main Category using completing-read
  (let* ((main-alist (mapcar (lambda (cat) 
                               (cons (format "[%s] %s" (nth 0 cat) (nth 1 cat)) cat))
                             my/parsed-org-structure))
         (main-selection (completing-read "Refile to category (↑↓ navigate, TAB complete): " main-alist nil t))
         (selected-main (cdr (assoc main-selection main-alist))))
    
    (if (not selected-main)
        (message "No category selected.")
      
      ;; Step 2: Select Sub Category using completing-read
      (let* ((sub-cats (nth 3 selected-main))
             (sub-alist (mapcar (lambda (sub)
                                  (cons (format "[%s] %s" (nth 0 sub) (nth 1 sub)) sub))
                                sub-cats))
             (sub-selection (completing-read "Refile to target (↑↓ navigate, TAB complete): " sub-alist nil t))
             (selected-sub (cdr (assoc sub-selection sub-alist))))
        
        (if (not selected-sub)
            (message "No target selected.")
          
          ;; Perform Refile Action
          (let* ((main-file (nth 2 selected-main))
                 (headline (nth 2 selected-sub))
                 (file-ov (nth 4 selected-sub))
                 (target-file (expand-file-name (or file-ov main-file) org-directory)))
            
            (unless (file-exists-p target-file)
              (make-directory (file-name-directory target-file) t)
              (with-temp-file target-file (insert "#+TITLE: " (file-name-base target-file) "\n")))

            (if (and headline (not (string-empty-p headline)))
                ;; Case A: Direct Refile to specific headline
                (if (string= headline "datetree")
                    (org-refile nil nil (list "Datetree" target-file nil nil))
                  ;; Standard Headline Refile
                  (let ((pos (save-excursion
                               (with-current-buffer (find-file-noselect target-file)
                                 (org-find-exact-headline-in-buffer headline)))))
                    (if pos
                        (progn
                          (org-refile nil nil (list headline target-file nil pos))
                          (message "Refiled to %s > %s" (file-name-base target-file) headline))
                      ;; Headline missing: create it or fail? Let's fail gracefully.
                      (message "Heading '%s' not found in %s. Please create it first." headline (file-name-base target-file)))))
              
              ;; Case B: Interactive Refile in Target File (HEADLINE is empty)
              (let ((org-refile-targets `((,target-file :maxlevel . 3))))
                (call-interactively 'org-refile)))))))))))

;; Auto-load structure on startup
(with-eval-after-load 'org
  (my/apply-org-structure))

(provide 'pkg-org)
;;; pkg-org.el ends here