-
-
Notifications
You must be signed in to change notification settings - Fork 56
/
denote-journal-extras.el
287 lines (242 loc) · 12.3 KB
/
denote-journal-extras.el
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
;;; denote-journal-extras.el --- Convenience functions for daily journaling -*- lexical-binding: t; -*-
;; Copyright (C) 2023-2024 Free Software Foundation, Inc.
;; Author: Protesilaos Stavrou <[email protected]>
;; Maintainer: Protesilaos Stavrou <[email protected]>
;; URL: https://github.com/protesilaos/denote
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This is a set of optional convenience functions that used to be
;; provided in the Denote manual. They facilitate the use of Denote
;; for daily journaling.
;;; Code:
(require 'denote)
(defgroup denote-journal-extras nil
"Denote for daily journaling."
:group 'denote
:link '(info-link "(denote) Top")
:link '(url-link :tag "Homepage" "https://protesilaos.com/emacs/denote"))
(defcustom denote-journal-extras-directory
(expand-file-name "journal" denote-directory)
"Directory for storing daily journal entries.
This can either be the same as the variable `denote-directory' or
a subdirectory of it.
A value of nil means to use the variable `denote-directory'.
Journal entries will thus be in a flat listing together with all
other notes. They can still be retrieved easily by searching for
the variable `denote-journal-extras-keyword'."
:group 'denote-journal-extras
:type '(choice (directory :tag "Provide directory path (is created if missing)")
(const :tag "Use the `denote-directory'" nil)))
(defcustom denote-journal-extras-keyword "journal"
"Single word keyword or list of keywords to tag journal entries.
It is used by `denote-journal-extras-new-entry' (or related)."
:group 'denote-journal-extras
:type '(choice (string :tag "Keyword")
(repeat :tag "List of keywords" string)))
(defcustom denote-journal-extras-title-format 'day-date-month-year-24h
"Date format to construct the title with `denote-journal-extras-new-entry'.
The value is either a symbol or an arbitrary string that is
passed to `format-time-string' (consult its documentation for the
technicalities).
Acceptable symbols and their corresponding styles are:
| Symbol | Style |
|-------------------------+-----------------------------------|
| day | Monday |
| day-date-month-year | Monday 19 September 2023 |
| day-date-month-year-24h | Monday 19 September 2023 20:49 |
| day-date-month-year-12h | Monday 19 September 2023 08:49 PM |
With a nil value, make `denote-journal-extras-new-entry' prompt
for a title."
:group 'denote-journal-extras
:type '(choice
(const :tag "Prompt for title with `denote-journal-extras-new-entry'" nil)
(const :tag "Monday"
:doc "The `format-time-string' is: %A"
day)
(const :tag "Monday 19 September 2023"
:doc "The `format-time-string' is: %A %e %B %Y"
day-date-month-year)
(const :tag "Monday 19 September 2023 20:49"
:doc "The `format-time-string' is: %A %e %B %Y %H:%M"
day-date-month-year-24h)
(const :tag "Monday 19 September 2023 08:49 PM"
:doc "The `format-time-string' is: %A %e %B %Y %I:%M %^p"
day-date-month-year-12h)
(string :tag "Custom string with `format-time-string' specifiers")))
(defcustom denote-journal-extras-hook nil
"Normal hook called after `denote-journal-extras-new-entry'.
Use this to, for example, set a timer after starting a new
journal entry (refer to the `tmr' package on GNU ELPA)."
:group 'denote-journal-extras
:type 'hook)
(defun denote-journal-extras-directory ()
"Make the variable `denote-journal-extras-directory' and its parents."
(if-let* (((stringp denote-journal-extras-directory))
(directory (file-name-as-directory (expand-file-name denote-journal-extras-directory))))
(progn
(when (not (file-directory-p denote-journal-extras-directory))
(make-directory directory :parents))
directory)
(denote-directory)))
(defun denote-journal-extras-keyword ()
"Return the value of the variable `denote-journal-extras-keyword' as a list."
(if (stringp denote-journal-extras-keyword)
(list denote-journal-extras-keyword)
denote-journal-extras-keyword))
(defun denote-journal-extras--keyword-regex ()
"Return a regular expression string that matches the journal keyword(s)."
(let ((keywords-sorted (mapcar #'regexp-quote (denote-keywords-sort (denote-journal-extras-keyword)))))
(concat "_" (string-join keywords-sorted ".*_"))))
(defun denote-journal-extras-file-is-journal-p (file)
"Return non-nil if FILE is a journal entry."
(and (denote-file-is-note-p file)
(string-match-p (denote-journal-extras--keyword-regex) (file-name-nondirectory file))))
(defun denote-journal-extras-filename-is-journal-p (filename)
"Return non-nil if FILENAME is a valid name for a journal entry."
(and (denote-filename-is-note-p filename)
(string-match-p (denote-journal-extras--keyword-regex) (file-name-nondirectory filename))))
(defun denote-journal-extras-daily--title-format (&optional date)
"Return present date in `denote-journal-extras-title-format' or prompt for title.
With optional DATE, use it instead of the present date. DATE has
the same format as that returned by `current-time'."
(format-time-string
(if (and denote-journal-extras-title-format
(stringp denote-journal-extras-title-format))
denote-journal-extras-title-format
(pcase denote-journal-extras-title-format
('day "%A")
('day-date-month-year "%A %e %B %Y")
('day-date-month-year-24h "%A %e %B %Y %H:%M")
('day-date-month-year-12h "%A %e %B %Y %I:%M %^p")
(_ (denote-title-prompt (format-time-string "%F" date)))))
date))
(defun denote-journal-extras--get-template ()
"Return template that has `journal' key in `denote-templates'.
If no template with `journal' key exists but `denote-templates'
is non-nil, prompt the user for a template among
`denote-templates'. Else return nil.
Also see `denote-journal-extras-new-entry'."
(if-let* ((template (alist-get 'journal denote-templates)))
template
(when denote-templates
(denote-template-prompt))))
;;;###autoload
(defun denote-journal-extras-new-entry (&optional date)
"Create a new journal entry in variable `denote-journal-extras-directory'.
Use the variable `denote-journal-extras-keyword' as a keyword for the
newly created file. Set the title of the new entry according to the
value of the user option `denote-journal-extras-title-format'.
With optional DATE as a prefix argument, prompt for a date. If
`denote-date-prompt-use-org-read-date' is non-nil, use the Org
date selection module.
When called from Lisp DATE is a string and has the same format as
that covered in the documentation of the `denote' function. It
is internally processed by `denote-valid-date-p'."
(interactive (list (when current-prefix-arg (denote-date-prompt))))
(let ((internal-date (or (denote-valid-date-p date) (current-time)))
(denote-directory (denote-journal-extras-directory)))
(denote
(denote-journal-extras-daily--title-format internal-date)
(denote-journal-extras-keyword)
nil nil date
(denote-journal-extras--get-template))
(run-hooks 'denote-journal-extras-hook)))
(defun denote-journal-extras--filename-date-regexp (&optional date)
"Regular expression to match journal entries for today or optional DATE.
DATE has the same format as that returned by `denote-valid-date-p'."
(let* ((identifier (format "%sT[0-9]\\{6\\}" (format-time-string "%Y%m%d" date)))
(order denote-file-name-components-order)
(id-index (seq-position order 'identifier))
(kw-index (seq-position order 'keywords)))
(if (> kw-index id-index)
(format "%s.*?_%s" identifier (denote-journal-extras--keyword-regex))
(format "_%s.*?@@%s" (denote-journal-extras--keyword-regex) identifier))))
(defun denote-journal-extras--entry-today (&optional date)
"Return list of files matching a journal for today or optional DATE.
DATE has the same format as that returned by `denote-valid-date-p'."
(denote-directory-files (denote-journal-extras--filename-date-regexp date)))
(define-obsolete-function-alias
'denote-journal-extra-path-to-new-or-existing-entry
'denote-journal-extras-path-to-new-or-existing-entry
"3.2.0")
;;;###autoload
(defun denote-journal-extras-path-to-new-or-existing-entry (&optional date)
"Return path to existing or new journal file.
With optional DATE, do it for that date, else do it for today. DATE is
a string and has the same format as that covered in the documentation of
the `denote' function. It is internally processed by
`denote-valid-date-p'.
If there are multiple journal entries for the date, prompt for one among
them using minibuffer completion. If there is only one, return it. If
there is no journal entry, create it."
(let* ((internal-date (or (denote-valid-date-p date) (current-time)))
(files (denote-journal-extras--entry-today internal-date)))
(cond
((length> files 1)
(completing-read "Select journal entry: " files nil t))
(files
(car files))
(t
(save-window-excursion
(denote-journal-extras-new-entry date)
(save-buffer)
(buffer-file-name))))))
;;;###autoload
(defun denote-journal-extras-new-or-existing-entry (&optional date)
"Locate an existing journal entry or create a new one.
A journal entry is one that has the value of the variable
`denote-journal-extras-keyword' as part of its file name.
If there are multiple journal entries for the current date,
prompt for one using minibuffer completion. If there is only
one, visit it outright. If there is no journal entry, create one
by calling `denote-journal-extra-new-entry'.
With optional DATE as a prefix argument, prompt for a date. If
`denote-date-prompt-use-org-read-date' is non-nil, use the Org
date selection module.
When called from Lisp, DATE is a string and has the same format
as that covered in the documentation of the `denote' function.
It is internally processed by `denote-valid-date-p'."
(interactive
(list
(when current-prefix-arg
(denote-date-prompt))))
(find-file (denote-journal-extras-path-to-new-or-existing-entry date)))
;;;###autoload
(defun denote-journal-extras-link-or-create-entry (&optional date id-only)
"Use `denote-link' on journal entry, creating it if necessary.
A journal entry is one that has the value of the variable
`denote-journal-extras-keyword' as part of its file name.
If there are multiple journal entries for the current date,
prompt for one using minibuffer completion. If there is only
one, link to it outright. If there is no journal entry, create one
by calling `denote-journal-extra-new-entry' and link to it.
With optional DATE as a prefix argument, prompt for a date. If
`denote-date-prompt-use-org-read-date' is non-nil, use the Org
date selection module.
When called from Lisp, DATE is a string and has the same format
as that covered in the documentation of the `denote' function.
It is internally processed by `denote-valid-date-p'.
With optional ID-ONLY as a prefix argument create a link that
consists of just the identifier. Else try to also include the
file's title. This has the same meaning as in `denote-link'."
(interactive
(pcase current-prefix-arg
('(16) (list (denote-date-prompt) :id-only))
('(4) (list (denote-date-prompt)))))
(let ((path (denote-journal-extras-path-to-new-or-existing-entry date)))
(denote-link path
(denote-filetype-heuristics (buffer-file-name))
(denote-get-link-description path)
id-only)))
(provide 'denote-journal-extras)
;;; denote-journal-extras.el ends here