-
Notifications
You must be signed in to change notification settings - Fork 6
/
org-node-seq.el
621 lines (551 loc) · 26.6 KB
/
org-node-seq.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
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
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
;;; org-node-seq.el --- Experimental way to define node sequences -*- lexical-binding: t; -*-
;; Copyright (C) 2024 Martin Edström
;;
;; 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 <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Support programmatically defining node sequences based on such
;; things as tags and date-stamps.
;; To Be Done: manually defining sequences
;;; Code:
(require 'seq)
(require 'subr-x)
(require 'cl-lib)
(require 'calendar)
(require 'transient)
(require 'compat)
(require 'org-node)
;;; Easy wrappers to define a sequence
;;;###autoload
(defun org-node-seq-def-on-any-sort-by-property
(key name prop &optional capture)
"Define a sequence sorted by property PROP.
If an ID-node does not have property PROP, it is excluded.
KEY, NAME and CAPTURE explained in `org-node-seq-defs'."
`(,key
:name ,name
:version 2
:capture ,capture
:classifier (lambda (node)
(let ((sortstr (cdr (assoc ,prop (org-node-get-props node)))))
(when (and sortstr (not (string-blank-p sortstr)))
(cons (concat sortstr " " (org-node-get-title node))
(org-node-get-id node)))))
:whereami (lambda ()
(when-let* ((sortstr (org-entry-get nil ,prop t))
(node (gethash (org-entry-get-with-inheritance "ID") org-nodes)))
(concat sortstr " " (org-node-get-title node))))
:prompter (lambda (key)
(let ((seq (cdr (assoc key org-node-seqs))))
(completing-read "Go to: " (plist-get seq :sorted-items))))
:try-goto (lambda (item)
(org-node-seq-try-goto-id (cdr item)))
:creator (lambda (sortstr key)
(let ((adder (lambda () (org-entry-put nil ,prop sortstr))))
(add-hook 'org-node-creation-hook adder)
(unwind-protect (org-node-create sortstr (org-id-new) key)
(remove-hook 'org-node-creation-hook adder))))))
;;;###autoload
(defun org-node-seq-def-on-tags-sort-by-property
(key name tags prop &optional capture)
"Define a sequence filtered by TAGS sorted by property PROP.
TAGS is a string of tags separated by colons.
KEY, NAME and CAPTURE explained in `org-node-seq-defs'."
`(,key
:name ,name
:version 2
:capture ,capture
:classifier (lambda (node)
(let ((sortstr (cdr (assoc ,prop (org-node-get-props node))))
(tagged (seq-intersection (split-string ,tags ":" t)
(org-node-get-tags-local node))))
(when (and sortstr tagged (not (string-blank-p sortstr)))
(cons (concat sortstr " " (org-node-get-title node))
(org-node-get-id node)))))
:whereami (lambda ()
(when (seq-intersection (split-string ,tags ":" t)
(org-get-tags))
(let ((sortstr (org-entry-get nil ,prop t))
(node (gethash (org-entry-get-with-inheritance "ID") org-nodes)))
(when (and sortstr node)
(concat sortstr " " (org-node-get-title node))))))
:prompter (lambda (key)
(let ((seq (cdr (assoc key org-node-seqs))))
(completing-read "Go to: " (plist-get seq :sorted-items))))
:try-goto (lambda (item)
(org-node-seq-try-goto-id (cdr item)))
;; NOTE: The sortstr should not necessarily become the title, but we make
;; it so anyway, and the user can edit afterwards.
;; REVIEW: This should probably change, better to prompt for title. But
;; how?
:creator (lambda (sortstr key)
(let ((adder (lambda ()
(org-entry-put nil ,prop sortstr)
(dolist (tag (split-string ,tags ":" t))
(org-node-tag-add tag)))))
(add-hook 'org-node-creation-hook adder)
(unwind-protect (org-node-create sortstr (org-id-new) key)
(remove-hook 'org-node-creation-hook adder))))))
;;;###autoload
(defun org-node-seq-def-on-filepath-sort-by-basename
(key name dir &optional capture date-picker)
"Define a sequence of files located under DIR.
The files need not contain a top-level property drawer with an ID, but
they do need to contain at least one ID-node.
KEY, NAME and CAPTURE explained in `org-node-seq-defs'.
When optional argument DATE-PICKER is non-nil, let the prompter use the
Org date picker. This needs file basenames in YYYY-MM-DD format."
(setq dir (abbreviate-file-name (file-truename dir)))
`(,key
:name ,name
:version 2
:capture ,capture
:classifier (lambda (node)
(when (string-prefix-p ,dir (org-node-get-file-path node))
(let* ((path (org-node-get-file-path node))
(sortstr (file-name-base path)))
(cons sortstr path))))
:whereami (lambda ()
(when (string-prefix-p ,dir buffer-file-truename)
(file-name-base buffer-file-truename)))
:prompter (lambda (key)
;; Tip: Consider `org-read-date-prefer-future' nil
(if ,date-picker
(let ((org-node-seq-that-marks-calendar key))
(org-read-date))
(let ((seq (cdr (assoc key org-node-seqs))))
(completing-read "Go to: " (plist-get seq :sorted-items)))))
:try-goto (lambda (item)
(org-node-seq-try-visit-file (cdr item)))
:creator (lambda (sortstr key)
(let ((org-node-creation-fn #'org-node-new-file)
(org-node-ask-directory ,dir))
(org-node-create sortstr (org-id-new) key)))))
;;; Helpers to use in a sequence definition
(defvar org-node-seq--guess-daily-dir nil
"Last result of function `org-node-seq--guess-daily-dir'.")
;;;###autoload
(defun org-node-seq--guess-daily-dir ()
"Do not rely on this.
Better insert a hardcoded string in your seq def
instead of calling this function."
(with-memoization org-node-seq--guess-daily-dir
(or (bound-and-true-p org-node-fakeroam-daily-dir)
(bound-and-true-p org-journal-dir)
(and (bound-and-true-p org-roam-directory)
(seq-find #'file-exists-p
(list (file-name-concat org-roam-directory "daily/")
(file-name-concat org-roam-directory "dailies/"))))
(seq-find #'file-exists-p
(list (file-name-concat org-directory "daily/")
(file-name-concat org-directory "dailies/"))))))
;;;###autoload
(defun org-node-seq-try-goto-id (id)
"Try to visit org-id ID and return non-nil, else nil on fail."
(let ((node (gethash id org-node--id<>node)))
(when node
(org-node--goto node)
t)))
;;;###autoload
(defun org-node-seq-try-visit-file (file)
"If FILE exists or a buffer has it as filename, visit that.
On success, return non-nil; else nil. Never create FILE anew."
(let ((buf (find-buffer-visiting file)))
(if buf
(switch-to-buffer buf)
(when (file-readable-p file)
(find-file file)))))
;;;###autoload
(defun org-node-seq-filename->ymd (path)
"Check the filename PATH for a date and return it.
On failing to coerce a date, return nil."
(when path
(let ((clipped-name (file-name-base path)))
(if (string-match
(rx bol (= 4 digit) "-" (= 2 digit) "-" (= 2 digit))
clipped-name)
(match-string 0 clipped-name)
;; Even in a non-daily file, pretend it is a daily if possible,
;; to allow entering the sequence at a more relevant date
(when-let ((stamp (org-node-extract-file-name-datestamp path)))
(org-node-seq-extract-ymd stamp org-node-datestamp-format))))))
;; TODO: Handle %s, %V, %y... is there a library?
;;;###autoload
(defun org-node-seq-extract-ymd (instance time-format)
"Try to extract a YYYY-MM-DD date out of string INSTANCE.
Assume INSTANCE is a string produced by TIME-FORMAT, e.g. if
TIME-FORMAT is %Y%m%dT%H%M%SZ then a possible INSTANCE is
20240814T123307Z. In that case, return 2024-08-14.
Will throw an error if TIME-FORMAT does not include either %F or
all three of %Y, %m and %d. May return odd results if other
format-constructs occur before these."
(let ((verify-re (org-node--make-regexp-for-time-format time-format)))
(when (string-match-p verify-re instance)
(let ((case-fold-search nil))
(let ((pos-year (string-search "%Y" time-format))
(pos-month (string-search "%m" time-format))
(pos-day (string-search "%d" time-format))
(pos-ymd (string-search "%F" time-format)))
(if (seq-some #'null (list pos-year pos-month pos-day))
(progn (cl-assert pos-ymd)
(substring instance pos-ymd (+ pos-ymd 10)))
(when (> pos-month pos-year) (cl-incf pos-month 2))
(when (> pos-day pos-year) (cl-incf pos-day 2))
(concat (substring instance pos-year (+ pos-year 4))
"-"
(substring instance pos-month (+ pos-month 2))
"-"
(substring instance pos-day (+ pos-day 2)))))))))
;;;; Sequence plumbing
(defcustom org-node-seq-defs nil
"Alist defining each node sequence.
This functionality is still experimental, and likely to have
higher-level wrappers in the future.
Each item looks like
\(KEY :name NAME
:classifier CLASSIFIER
:whereami WHEREAMI
:prompter PROMPTER
:try-goto TRY-GOTO
:creator CREATOR
:capture CAPTURE
:version VERSION)
KEY uniquely identifies the sequence, and is the key to type after
\\[org-node-seq-dispatch] to select it. It may not be \"j\",
\"n\", \"p\" or \"c\", these keys are reserved for
Jump/Next/Previous/Capture actions.
NAME describes the sequence, in one or a few words.
CLASSIFIER is a single-argument function taking an `org-node'
object and should return a cons cell or list if a sequence-item was
found, otherwise nil.
The list may contain anything, but the first element must be a
sort-string, i.e. a string suitable for sorting on. An example
is a date in the format YYYY-MM-DD, but not in the format MM/DD/YY.
This is what determines the order of items in the sequence: after
all nodes have been processed by CLASSIFIER, the non-nil return
values are sorted by the sort-string, using `string>'.
Aside from returning a single item, CLASSIFIER may also return a list of
such items. This can be useful if e.g. you have a special type of node
that \"defines\" a sequence by simply containing links to each item that
should go into it.
Function PROMPTER may be used during jump/capture/refile to
interactively prompt for a sort-string. This highlights the
other use of the sort-string: finding our way back from scant
context.
For example, in a sequence of daily-notes sorted on YYYY-MM-DD, a
prompter could use `org-read-date'.
PROMPTER receives one argument, the sequence plist, which has the
same form as one of the values in `org-node-seq-defs' but
includes two extra members :key, corresponding to KEY, and
:sorted-items, which may be useful for interactive completion.
Function WHEREAMI is like PROMPTER in that it should return a
sort-string. However, it should do this without user
interaction, and may return nil. For example, if the user is not
currently in a daily-note, the daily-notes\\=' WHEREAMI should
return nil. It receives no arguments.
Function TRY-GOTO takes a single argument: one of the items
originally created by CLASSIFIER. That is, a list of not only a
sort-string but any associated data you put in. If TRY-GOTO
succeeds in using this information to visit a place of interest,
it should return non-nil, otherwise nil. It should not create or
write anything on failure - reserve that for the CREATOR
function.
Function CREATOR creates a place that did not exist. For
example, if the user picked a date from `org-read-date' but no
daily-note exists for that date, CREATOR is called to create that
daily-note. It receives a would-be sort-string as argument.
Optional string CAPTURE indicates the keys to a capture template
to autoselect, when you choose the capture option in the
`org-node-seq-dispatch' menu.
Integer VERSION indicates the sequence definition language. New
sequence should use version 2, as of 2024-09-05. When org-node
updates the sequence definition language, old versions may still
work, but this is not heavily tested, so it will start printing a
message to remind you to check out the wiki on GitHub and port
your definitions."
:type 'alist
:group 'org-node
:package-version '(org-node . "1.0.10")
:set #'org-node--set-and-remind-reset)
;;;###autoload
(defvar org-node-seqs nil
"Alist of data for each node sequence.")
(defun org-node-seq--add-item (&optional key)
"Analyze node near point to maybe grow a node seq.
The sequence is identified either by KEY, or if that is nil, by the
current value of `org-node-proposed-sequence'. If that is also nil, do
nothing."
(when (or key org-node-proposed-sequence)
(let* ((seq (cdr (assoc (or key org-node-proposed-sequence)
org-node-seqs)))
(node-here (gethash (org-entry-get-with-inheritance "ID") org-nodes))
(new-item (when node-here
(funcall (plist-get seq :classifier) node-here))))
(when new-item
(unless (member new-item (plist-get seq :sorted-items))
(push new-item (plist-get seq :sorted-items))
(sort (plist-get seq :sorted-items)
(lambda (item1 item2)
(string> (car item1) (car item2)))))))))
(defun org-node-seq--jump (key)
"Prompt for and jump to an entry in node seq identified by KEY."
(let* ((seq (cdr (assoc key org-node-seqs)))
(sortstr (if (eq 2 (plist-get seq :version))
(funcall (plist-get seq :prompter) key)
(funcall (plist-get seq :prompter) seq)))
(item (assoc sortstr (plist-get seq :sorted-items))))
(if item
(unless (funcall (plist-get seq :try-goto) item)
(delete item (plist-get seq :sorted-items))
(if (eq 2 (plist-get seq :version))
(funcall (plist-get seq :creator) sortstr key)
(funcall (plist-get seq :creator) sortstr)))
(if (eq 2 (plist-get seq :version))
(funcall (plist-get seq :creator) sortstr key)
(funcall (plist-get seq :creator) sortstr)))))
(defun org-node-seq--goto-next (key)
"Visit the next entry in node seq identified by KEY."
(org-node-seq--goto-previous key t))
(defun org-node-seq--goto-previous (key &optional next)
"Visit the previous entry in node seq identified by KEY.
With non-nil argument NEXT, visit the next entry, not previous."
(let* ((seq (cdr (assoc key org-node-seqs)))
(tail (plist-get seq :sorted-items))
head
here)
(unless tail
(error "No items in sequence \"%s\"" (plist-get seq :name)))
;; Depending on the design of the :whereami lambda, being in a sub-heading
;; may block discovering that a parent heading is a member of the sequence,
;; so re-try until the top level
(when (derived-mode-p 'org-mode)
(save-excursion
(without-restriction
(while (and (not (setq here (funcall (plist-get seq :whereami))))
(org-up-heading-or-point-min))))))
(when (or (when here
;; Find our location in the sequence
(cl-loop for item in tail
while (string> (car item) here)
do (push (pop tail) head))
(when (equal here (caar tail))
(pop tail)
;; Opportunistically clean up duplicate keys
(while (equal here (caar tail))
(setcar tail (cadr tail))
(setcdr tail (cddr tail))))
t)
(when (y-or-n-p
(format "Not in sequence \"%s\". Jump to latest item in that sequence?"
(plist-get seq :name)))
(setq head (take 1 tail))
t))
;; Usually this should return on the first try, but sometimes stale
;; items refer to something that has been erased from disk, so
;; deregister each item that TRY-GOTO failed to visit, and try again.
(cl-loop for item in (if next head tail)
if (funcall (plist-get seq :try-goto) item)
return t
else do (delete item (plist-get seq :sorted-items))
finally do (message "No %s item in sequence \"%s\""
(if next "next" "previous")
(plist-get seq :name))))))
(defvar org-node-seq--current-key nil
"Key identifying the node seq currently being browsed with the menu.
Unlike `org-node-proposed-sequence', does not need to revert to nil.")
(defun org-node-seq-capture-target ()
"Experimental."
(org-node-cache-ensure)
(let ((key (or org-node-seq--current-key
(let* ((valid-keys (mapcar #'car org-node-seq-defs))
(elaborations
(cl-loop for seq in org-node-seq-defs
concat
(format " %s(%s)"
(car seq)
(plist-get (cdr seq) :name))))
(input (read-char-from-minibuffer
(format "Press any of [%s] to capture into sequence: %s "
(string-join valid-keys ",")
elaborations)
(mapcar #'string-to-char valid-keys))))
(char-to-string input)))))
;; Almost identical to `org-node-seq--jump'
(let* ((seq (cdr (assoc key org-node-seqs)))
(sortstr (or org-node-proposed-title
(if (eq 2 (plist-get seq :version))
(funcall (plist-get seq :prompter) key)
(funcall (plist-get seq :prompter) seq))))
(item (assoc sortstr (plist-get seq :sorted-items))))
(when (or (null item)
(not (funcall (plist-get seq :try-goto) item)))
;; TODO: Move point after creation to most appropriate place
(if (eq 2 (plist-get seq :version))
(funcall (plist-get seq :creator) sortstr key)
(funcall (plist-get seq :creator) sortstr))))))
(defun org-node-seq--build-from-def (def)
"From DEF, make a plist for `org-node-seqs'.
DEF is a seq-def from `org-node-seq-defs'."
(let ((classifier (org-node--ensure-compiled
(plist-get (cdr def) :classifier))))
(nconc
(cl-loop for elt in (cdr def)
if (functionp elt)
collect (org-node--ensure-compiled elt)
else collect elt)
(cl-loop for node being the hash-values of org-node--id<>node
as result = (funcall classifier node)
if (listp (car result))
nconc result into items
else collect result into items
finally return
;; Sort `string>' due to most recent dailies probably being most
;; relevant, thus cycling recent dailies will be best perf.
(list :key (car def)
:sorted-items (delete-consecutive-dups
(if (< emacs-major-version 30)
;; Faster than compat's sort on 29
(cl-sort items #'string> :key #'car)
;; Will run new builtin sort on 30
(compat-call sort items
:key #'car :lessp #'string<
:reverse t :in-place t))))))))
(defun org-node-seq--add-to-dispatch (key name)
"Use KEY and NAME to add a sequence to the Transient menu."
(when (ignore-errors (transient-get-suffix 'org-node-seq-dispatch key))
(transient-remove-suffix 'org-node-seq-dispatch key))
(transient-append-suffix 'org-node-seq-dispatch '(0 -1)
(list key name key))
;; Make the sequence switches mutually exclusive
(let ((old (car (slot-value (get 'org-node-seq-dispatch 'transient--prefix)
'incompatible))))
(setf (slot-value (get 'org-node-seq-dispatch 'transient--prefix)
'incompatible)
(list (seq-uniq (cons key old))))))
;; These suffixes just exist due to a linter complaint, could
;; have been lambdas
(transient-define-suffix org-node-seq--goto-previous* (args)
(interactive (list (transient-args 'org-node-seq-dispatch)))
(if args
(org-node-seq--goto-previous (car args))
(message "Choose sequence before navigating")))
(transient-define-suffix org-node-seq--goto-next* (args)
(interactive (list (transient-args 'org-node-seq-dispatch)))
(if args
(org-node-seq--goto-next (car args))
(message "Choose sequence before navigating")))
(transient-define-suffix org-node-seq--jump* (args)
(interactive (list (transient-args 'org-node-seq-dispatch)))
(if args
(org-node-seq--jump (car args))
(message "Choose sequence before navigating")))
(transient-define-suffix org-node-seq--capture (args)
(interactive (list (transient-args 'org-node-seq-dispatch)))
(if args
(progn (setq org-node-seq--current-key (car args))
(unwind-protect
(let* ((seq (cdr (assoc (car args) org-node-seqs)))
(capture-keys (plist-get seq :capture)))
(if capture-keys
(org-capture nil capture-keys)
(message "No capture template for sequence %s"
(plist-get seq :name))))
(setq org-node-seq--current-key nil)))
(message "Choose sequence before navigating")))
;;;###autoload (autoload 'org-node-seq-dispatch "org-node" nil t)
(transient-define-prefix org-node-seq-dispatch ()
["Sequence"
("|" "Invisible" "Placeholder" :if-nil t)]
["Navigation"
("p" "Previous in sequence" org-node-seq--goto-previous* :transient t)
("n" "Next in sequence" org-node-seq--goto-next* :transient t)
("j" "Jump (or create)" org-node-seq--jump*)
("c" "Capture into" org-node-seq--capture)])
(defcustom org-node-seq-that-marks-calendar nil
"Key for the sequence that should mark days in the calendar.
This affects the appearance of the `org-read-date' calendar
popup. For example, you can use it to indicate which days have a
daily-journal entry.
This need usually not be customized! When you use
`org-node-seq-dispatch' to jump to a daily-note or some
other date-based sequence, that sequence may be designed to
temporarily set this variable.
Customize this mainly if you want a given sequence to always be
indicated, any time Org pops up a calendar for you.
The sort-strings in the sequence that corresponds to this key
should be correctly parseable by `parse-time-string'."
:group 'org-node
:type '(choice key (const nil)))
;; TODO: How to cooperate with preexisting marks?
(defun org-node-seq--mark-days ()
"Mark days in the calendar popup.
The user option `org-node-seq-that-marks-calendar' controls
which dates to mark.
Meant to sit on these hooks:
- `calendar-today-invisible-hook'
- `calendar-today-visible-hook'"
(calendar-unmark)
(when org-node-seq-that-marks-calendar
(let* ((seq (cdr (assoc org-node-seq-that-marks-calendar
org-node-seqs)))
(sortstrs (mapcar #'car (plist-get seq :sorted-items)))
mdy)
(dolist (date sortstrs)
;; Use `parse-time-string' rather than `iso8601-parse' to fail quietly
(setq date (parse-time-string date))
(when (seq-some #'natnump date) ;; Basic check that it could be parsed
(setq mdy (seq-let (_ _ _ d m y _ _ _) date
(list m d y)))
(when (calendar-date-is-visible-p mdy)
(calendar-mark-visible-date mdy)))))))
;; Not used inside this package; a convenience for users.
(defun org-node-seq-goto (key sortstr)
"Visit an entry in sequence identified by KEY.
The entry to visit has sort-string SORTSTR. Create if it does
not exist."
(let* ((seq (cdr (assoc key org-node-seqs)))
(item (assoc sortstr (plist-get seq :sorted-items))))
(when (or (null item)
(if (funcall (plist-get seq :try-goto) item)
nil
(delete item (plist-get seq :sorted-items))
t))
(funcall (plist-get seq :creator) sortstr key))))
(defun org-node-seq--reset ()
"Wipe and re-build all sequences.
Must be done after the main org-node cache is up to date."
(setq org-node-seqs nil)
(dolist (def org-node-seq-defs)
(setf (alist-get (car def) org-node-seqs nil nil #'equal)
(org-node-seq--build-from-def def))
;; TODO: Clear any old sequence from menu
(org-node-seq--add-to-dispatch (car def)
(plist-get (cdr def) :name))))
(defun org-node-seq--enable-or-disable ()
"If `org-node-cache-mode' is enabled, enable node sequences as well."
(if org-node-cache-mode
(progn
;; FIXME: A dirty-added node eventually disappears if its buffer is
;; never saved, and then the node seq stops working
(add-hook 'org-node-creation-hook #'org-node-seq--add-item 90)
(add-hook 'calendar-today-invisible-hook #'org-node-seq--mark-days 5)
(add-hook 'calendar-today-visible-hook #'org-node-seq--mark-days 5)
(add-hook 'org-node--mid-scan-hook #'org-node-seq--reset))
(remove-hook 'org-node-creation-hook #'org-node-seq--add-item)
(remove-hook 'calendar-today-invisible-hook #'org-node-seq--mark-days)
(remove-hook 'calendar-today-visible-hook #'org-node-seq--mark-days)
(remove-hook 'org-node--mid-scan-hook #'org-node-seq--reset)))
(org-node-seq--enable-or-disable)
(add-hook 'org-node-cache-mode-hook #'org-node-seq--enable-or-disable)
(provide 'org-node-seq)
;;; org-node-seq.el ends here