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
|
;;; org-capture-config.el --- Org Capture Configuration -*- lexical-binding: t; coding: utf-8; -*-
;; author: Craig Jennings <c@cjennings.net>
;;; Commentary:
;;
;; Layer: 3 (Domain Workflow).
;; Category: D/P.
;; Load shape: eager.
;; Eager reason: capture is a daily hot path; org-protocol capture handlers must
;; be registered for external capture to work.
;; Top-level side effects: capture templates and org-protocol handlers via
;; use-package.
;; Runtime requires: none (configures packages via use-package).
;; Direct test load: yes.
;;
;; Customizations related to org-capture and org-refile.
;; Includes capture templates for tasks, links, PDFs, EPUBs, emails, and drill questions.
;; bookmarklet code:
;; text
;; javascript:location.href='org-protocol://capture?template=L&url=%27+encodeURIComponent(location.href)+%27&title=%27+encodeURIComponent(document.title)+%27&body=%27+encodeURIComponent(window.getSelection())
;; text + selection
;; javascript:location.href='org-protocol://capture?template=p&url=%27+encodeURIComponent(location.href)+%27&title=%27+encodeURIComponent(document.title)+%27&body=%27+encodeURIComponent(window.getSelection())
;;; Code:
(defvar org-capture-plist)
(defvar org-capture-templates)
(defvar org-complex-heading-regexp-format)
(declare-function cj/--drill-pick-file "org-drill-config")
(declare-function org-at-encrypted-entry-p "org-crypt")
(declare-function org-at-heading-p "org")
(declare-function org-back-to-heading "org")
(declare-function org-capture-expand-file "org-capture")
(declare-function org-capture-get "org-capture")
(declare-function org-capture-put "org-capture")
(declare-function org-capture-put-target-region-and-position "org-capture")
(declare-function org-capture-target-buffer "org-capture")
(declare-function org-display-warning "org")
(declare-function org-get-heading "org")
(declare-function org-parse-time-string "org")
(declare-function pdf-view-active-region-text "pdf-view")
(defvar cj/org-capture--file-headline-target-cache (make-hash-table :test #'equal)
"Cache Org capture file+headline target markers by expanded file and headline.")
(defun cj/org-capture-clear-target-cache ()
"Clear cached Org capture target markers."
(interactive)
(clrhash cj/org-capture--file-headline-target-cache)
(message "Cleared org-capture target cache"))
(defun cj/org-capture--file-headline-target-p (target)
"Return non-nil when TARGET is an Org capture file+headline target."
(pcase target
(`(file+headline ,_path ,(and _headline (pred stringp))) t)
(_ nil)))
(defun cj/org-capture--headline-marker-valid-p (marker headline)
"Return non-nil when MARKER still points at HEADLINE."
(and (markerp marker)
(marker-buffer marker)
(buffer-live-p (marker-buffer marker))
(with-current-buffer (marker-buffer marker)
(save-excursion
(goto-char marker)
(and (derived-mode-p 'org-mode)
(org-at-heading-p)
(string= (org-get-heading t t t t) headline))))))
(defun cj/org-capture--file-headline-cache-key (path headline)
"Return the cache key for PATH and HEADLINE."
(list (org-capture-expand-file path) headline))
(defun cj/org-capture--goto-file-headline (path headline)
"Move to capture target PATH and HEADLINE, using a cached marker when valid.
This implements Org's `file+headline' target positioning behavior, but avoids
re-scanning large target files after the first successful lookup."
(set-buffer (org-capture-target-buffer path))
;; Org expects the target file to be in Org mode, otherwise it throws an
;; error. Match Org's stock file+headline behavior here.
(unless (derived-mode-p 'org-mode)
(org-display-warning
(format "Capture requirement: switching buffer %S to Org mode"
(current-buffer)))
(org-mode))
(org-capture-put-target-region-and-position)
(widen)
(let* ((key (cj/org-capture--file-headline-cache-key path headline))
(marker (gethash key cj/org-capture--file-headline-target-cache)))
(if (cj/org-capture--headline-marker-valid-p marker headline)
(goto-char marker)
(goto-char (point-min))
(if (re-search-forward (format org-complex-heading-regexp-format
(regexp-quote headline))
nil t)
(forward-line 0)
(goto-char (point-max))
(unless (bolp) (insert "\n"))
(insert "* " headline "\n")
(forward-line -1))
(puthash key (copy-marker (point))
cj/org-capture--file-headline-target-cache))))
(defun cj/org-capture--set-file-headline-target-location (target)
"Set Org capture target location for file+headline TARGET."
(pcase target
(`(file+headline ,path ,headline)
(let ((target-entry-p t))
(save-excursion
(cj/org-capture--goto-file-headline path headline)
(org-capture-put :buffer (current-buffer)
:pos (point)
:target-entry-p target-entry-p
:decrypted
(and (featurep 'org-crypt)
(org-at-encrypted-entry-p)
(save-excursion
(org-decrypt-entry)
(and (org-back-to-heading t) (point))))))))))
(defun cj/org-capture--set-target-location-advice (orig-fun &optional target)
"Use cached target lookup around ORIG-FUN for file+headline capture targets."
(let ((resolved-target (or target (org-capture-get :target))))
(if (cj/org-capture--file-headline-target-p resolved-target)
(cj/org-capture--set-file-headline-target-location resolved-target)
(funcall orig-fun target))))
(with-eval-after-load 'org-capture
(advice-add 'org-capture-set-target-location
:around #'cj/org-capture--set-target-location-advice))
;; --------------------------- Org-Capture Templates ---------------------------
;; you can bring up the org capture menu with C-c c
(use-package org-protocol
:ensure nil ;; built-in
:after org
:config
;; -------------------------- Event Capture Formatting -------------------------
(defun cj/org-capture--date-prefix (timestamp)
"Return \"YY-MM-DD: \" prefix from org TIMESTAMP string, or nil if unparseable."
(when-let ((parsed (ignore-errors (org-parse-time-string timestamp))))
(let ((year (nth 5 parsed))
(month (nth 4 parsed))
(day (nth 3 parsed)))
(when (and year month day
(> year 0) (> month 0) (> day 0))
(format "%02d-%02d-%02d: "
(mod year 100) month day)))))
(defun cj/org-capture-format-event-headline ()
"Format the event headline with YY-MM-DD prefix from the WHEN timestamp.
This function is called during `org-capture' finalization to prepend the date
to the event title for better organization in the schedule file."
(when (string= (plist-get org-capture-plist :key) "e")
(save-excursion
(goto-char (point-min))
(when (re-search-forward "^WHEN: \\(<[^>]+>\\)" nil t)
(let ((date-prefix (cj/org-capture--date-prefix (match-string 1))))
(when date-prefix
(goto-char (point-min))
(when (looking-at "^\\(\\*+ \\)\\(.*\\)$")
(replace-match (concat "\\1" date-prefix "\\2")))))))))
(defun cj/org-capture-event-content ()
"Get the appropriate content for event capture based on context.
Returns the selected text from either Emacs or browser (via org-protocol)
formatted appropriately for insertion into the capture template."
(cond
;; If called from org-protocol (browser), get the initial from org-store-link-plist
((and (boundp 'org-store-link-plist)
org-store-link-plist
(let ((val (plist-get org-store-link-plist :initial)))
(and (stringp val) (not (string-empty-p val)))))
(concat "\n" (plist-get org-store-link-plist :initial)))
;; If there's a selected region in Emacs, use it from capture plist
((and (stringp (plist-get org-capture-plist :initial))
(not (string= (plist-get org-capture-plist :initial) "")))
(concat "\n" (plist-get org-capture-plist :initial)))
;; Otherwise, return empty string
(t "")))
;; ----------------------- Org Capture PDF Active Region -----------------------
(defun cj/org-capture-pdf-active-region ()
"Capture the active region of the pdf-view buffer.
Intended to be called within an org capture template."
(let* ((pdf-buf-name (plist-get org-capture-plist :original-buffer))
(pdf-buf (get-buffer pdf-buf-name)))
(if (buffer-live-p pdf-buf)
(with-current-buffer pdf-buf
(car (pdf-view-active-region-text)))
(user-error "Buffer %S not alive" pdf-buf-name))))
;; ORG-CAPTURE TEMPLATES
(setq org-protocol-default-template-key "L")
(setq org-capture-templates
'(("t" "Task" entry (file+headline inbox-file "Inbox")
"* TODO %?" :prepend t)
("e" "Event" entry (file+headline schedule-file "Scheduled Events")
"* %?%:description
SCHEDULED: %^t%(cj/org-capture-event-content)
Captured On: %U"
:prepend t
:prepare-finalize cj/org-capture-format-event-headline)
("E" "Epub Text" entry (file+headline inbox-file "Inbox")
"* %?
#+BEGIN_QUOTE\n %i\n#+END_QUOTE
Source: [[%:link][%(buffer-name (org-capture-get :original-buffer))]]
Captured On: %U" :prepend t)
;; requires cj/org-capture-pdf-active-region function defined above
("P" "PDF Text" entry (file+headline inbox-file "Inbox")
"* %?
#+BEGIN_QUOTE\n%(cj/org-capture-pdf-active-region)\n#+END_QUOTE
Source: [[%L][%(buffer-name (org-capture-get :original-buffer))]]
Captured On: %U" :prepend t)
("p" "Link with Selection" entry (file+headline inbox-file "Inbox")
"* %?%:description
#+BEGIN_QUOTE\n%i\n#+END_QUOTE
[[%:link][%:description]]
Captured On: %U\n" :prepend t :immediate-finish t)
("L" "Link" entry (file+headline inbox-file "Inbox")
"* %?%:description
[[%:link][%:description]]\nCaptured On: %U" :prepend t :immediate-finish t)
("m" "Mu4e Email" entry (file+headline inbox-file "Inbox")
"* TODO %?
%(if (string= \"%i\" \"\") \"\" \"\n#+BEGIN_QUOTE\n%i\n#+END_QUOTE\")
[[%:link][%:description]]
Captured On: %U"
:prepend t)
("d" "Drill Question" entry
(file (lambda () (cj/--drill-pick-file drill-dir)))
"* Item :drill:\n%?
** Answer\n%i\nSource: [[%:link][%:description]]
Captured On: %U" :prepend t)
("f" "Drill Question (from PDF)" entry
(file (lambda () (cj/--drill-pick-file drill-dir)))
"* Item :drill:\n%?
** Answer\n%(cj/org-capture-pdf-active-region)
Source: [[%L][%(buffer-name (org-capture-get :original-buffer))]]
Captured On: %U" :prepend t)
)) ;; end setq
) ;; end use-package org-protocol
(provide 'org-capture-config)
;;; org-capture-config.el ends here.
|