-
Notifications
You must be signed in to change notification settings - Fork 104
/
org-brain.el
3552 lines (3191 loc) · 152 KB
/
org-brain.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
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; org-brain.el --- Org-mode concept mapping -*- lexical-binding: t; -*-
;; Copyright (C) 2017--2020 Erik Sjöstrand
;; MIT License
;; Author: Erik Sjöstrand <[email protected]>
;; URL: http://github.com/Kungsgeten/org-brain
;; Keywords: outlines hypermedia
;; Package-Requires: ((emacs "25.1") (org "9.2"))
;; Version: 0.94
;;; Commentary:
;; org-brain implements a variant of concept mapping with org-mode, it is
;; inspired by The Brain software (http://thebrain.com). An org-brain is a
;; network of org-mode entries, where each entry is a file or a headline, and
;; you can get a visual overview of the relationships between the entries:
;; parents, children, siblings and friends. This visual overview can also be
;; used to browse your entries. You can think of entries as nodes in a mind map,
;; or pages in a wiki.
;; All org files put into your `org-brain-path' directory will be considered
;; entries in your org-brain. Headlines with an ID property in your entry file(s)
;; are also considered as entries.
;; Use `org-brain-visualize' to see the relationships between entries, quickly
;; add parents/children/friends/pins to an entry, and open them for editing.
;;; Code:
(require 'org-element)
(require 'org-attach)
(require 'org-agenda)
(require 'org-macs)
(require 'org-id)
(require 'picture)
(require 'subr-x)
(require 'seq)
(defgroup org-brain nil
"Org-mode concept mapping"
:prefix "org-brain-"
:group 'org)
;;;; Custom vars
(defcustom org-brain-path (file-truename (expand-file-name "brain" org-directory))
"The root directory of your org-brain.
`org-mode' files placed in this directory, or its subdirectories,
will be considered org-brain entries."
:group 'org-brain
:type '(directory))
(defcustom org-brain-scan-directories-recursively t
"If subdirectories inside `org-brain-path' are considered part of the brain or not."
:group 'org-brain
:type '(boolean))
(defcustom org-brain-files-extension "org"
"The extension for entry files in `org-brain-path'."
:group 'org-brain
:type '(string))
(defcustom org-brain-ignored-resource-links '("fuzzy" "radio" "brain-child" "brain-parent" "brain-friend")
"`org-link-types' which shouldn't be shown as resources in `org-brain-visualize'."
:group 'org-brain
:type '(repeat string))
(defcustom org-brain-backlink nil
"If backlink resource should be added when creating a brain org-link.
This only works when completing the link via `org-insert-link'.
Example: If you create a brain-link in A to B, setting this
variable to non-nil would also create A as a resource in B.
If this variable is a string it will be added as a prefix in the backlink.
Example: \"<--\" would add \"<--A\" in the example above."
:group 'org-brain
:type '(restricted-sexp :match-alternatives
(stringp 't 'nil)))
(defcustom org-brain-backlink-heading t
"If the org heading should be used when creating a backlink.
Example: Creating a brain-link in A to B and A is an org file with the headings:
* Parent header
** Child
[brain:linkToB]
Setting this variable to t will create the following backlink in B:
[[file:A.org::*Child][Parent header > Child]]."
:group 'org-brain
:type '(boolean))
(make-obsolete-variable 'org-brain-suggest-stored-link-as-resource
"org-brain-suggest-stored-link-as-resource isn't needed because of `org-insert-link-global'."
"0.6")
(defcustom org-brain-data-file (file-truename (expand-file-name ".org-brain-data.el" org-brain-path))
"Where org-brain data is saved."
:group 'org-brain
:type '(directory))
(load org-brain-data-file t t)
(defcustom org-brain-visualize-default-choices 'all
"Which entries to choose from when using `org-brain-visualize'.
If 'all, choose from all file and headline entries.
If 'files, only choose from file entries.
If 'root, only choose from file entries in `org-brain-path' (non-recursive)."
:group 'org-brain
:type '(choice
(const :tag "All entries" all)
(const :tag "Only file entries" files)
(const :tag "Only root file entries" root)))
(defcustom org-brain-include-file-entries t
"If set to nil `org-brain' is optimized for headline entries.
Only headlines will be considered as entries when visualizing."
:group 'org-brain
:type '(boolean))
(make-obsolete-variable
'org-brain-file-from-input-function
"`org-brain-default-file-parent' can be used as a better alternative."
"0.92")
(defcustom org-brain-default-file-parent nil
"Where to store new entries with unspecified local parent.
For instance if creating a new entry with `org-brain-visualize'.
If nil, create the new entry as a file entry relative to `org-brain-path'.
If set to a string it should be a file entry. That entry will be used as the
local parent and the new entry will be a headline."
:group 'org-brain
:type '(choice string (const nil)))
(defcustom org-brain-show-full-entry nil
"Always show entire entry contents?"
:group 'org-brain
:type '(boolean))
(defcustom org-brain-show-resources t
"Should entry resources be shown in `org-brain-visualize'?"
:group 'org-brain
:type '(boolean))
(defcustom org-brain-show-text t
"Should the entry text be shown in `org-brain-visualize'?"
:group 'org-brain
:type '(boolean))
(defcustom org-brain-show-history t
"Should the navigation history be shown in `org-brain-visualize'?"
:group 'org-brain
:type '(boolean))
(defcustom org-brain-show-icons t
"Should icons from `org-agenda-category-icon-alist' be shown when visualizing?"
:group 'org-brain
:type '(boolean))
(defcustom org-brain-category-icon-width 2
"The character width of icons."
:group 'org-brain
:type '(integer))
(defcustom org-brain-quit-after-goto nil
"Should the *org-brain* buffer window close itself after executing a goto command?"
:group 'org-brain
:type '(boolean))
(defcustom org-brain-headline-links-only-show-visible t
"Only show visible parts (descriptions) of headline links.
See the docstring for `org-brain-headline-at' for more info
on how this is implemented."
:group 'org-brain
:type '(boolean))
(defcustom org-brain-file-entries-use-title t
"If file entries should show their title, when choosing entries from a list.
This can potentially be slow. If set to nil, the relative
filenames will be shown instead, which is faster."
:group 'org-brain
:type '(boolean))
(defcustom org-brain-scan-for-header-entries t
"If org-brain should scan for header entries inside files.
Useful if you don't tend to use header entries in your workflow,
since scanning can be slow in long file entries.
This only affects selection prompts and not functions like `org-brain-headline-to-file'."
:group 'org-brain
:type '(boolean))
(defcustom org-brain-headline-entry-name-format-string "%s::%s"
"How headline entries are represented when choosing entries.
This `format' string is used in `org-brain-entry-name' for headline entries.
`format' gets two objects: the file and the headline."
:group 'org-brain
:type '(string))
(defcustom org-brain-visualize-text-hook nil
"Hook runs after inserting `org-brain-text' in `org-brain-visualize'.
Can be used to prettify the entry text, e.g.
`org-display-inline-images'."
:group 'org-brain
:type 'hook)
(defcustom org-brain-after-visualize-hook nil
"Hook run after `org-brain-visualize', but before `org-brain-text'.
Can be used to prettify the buffer output, e.g. `ascii-art-to-unicode'."
:group 'org-brain
:type 'hook)
(defcustom org-brain-new-entry-hook nil
"Hook run after a new headline entry has been created."
:group 'org-brain
:type 'hook)
(defcustom org-brain-visualize-follow-hook nil
"Hook run after viewing an entry by means of `org-brain-visualize-follow'."
:group 'org-brain
:type 'hook)
(defcustom org-brain-after-resource-button-functions nil
"Hook run during `org-brain-insert-resource-button'.
Insert a bullet, then run hook functions, then insert the actual button.
Each function must take a single argument: the org link to the resource.
Can for instance be used in combination with `all-the-icons'."
:group 'org-brain
:type 'hook)
(defcustom org-brain-vis-title-prepend-functions '(org-brain-entry-icon)
"Functions which `org-brain-vis-title' use before inserting the entry title.
Each function should take the entry as the only argument, and
should return a string. The strings are prepended to the entry title."
:group 'org-brain
:type 'hook
:options '(org-brain-entry-icon
org-brain-entry-todo-state
org-brain-entry-tags-string))
(defcustom org-brain-vis-title-append-functions '()
"Functions which `org-brain-vis-title' use after inserting the entry title.
Each function should take the entry as the only argument, and
should return a string. The strings are appended to the entry title."
:group 'org-brain
:type 'hook
:options '(org-brain-entry-icon
org-brain-entry-todo-state
org-brain-entry-tags-string))
(defcustom org-brain-vis-current-title-prepend-functions '()
"Like `org-brain-vis-title-prepend-functions' for the current visualized entry.
First `org-brain-vis-title-prepend-functions' are ran, and then these."
:group 'org-brain
:type 'hook
:options '(org-brain-entry-icon
org-brain-entry-todo-state
org-brain-entry-tags-string))
(defcustom org-brain-vis-current-title-append-functions '()
"Like `org-brain-vis-title-append-functions' for the current visualized entry.
First `org-brain-vis-title-append-functions' are ran, and then these."
:group 'org-brain
:type 'hook
:options '(org-brain-entry-icon
org-brain-entry-todo-state
org-brain-entry-tags-string))
(defcustom org-brain-exclude-text-tag "notext"
"`org-mode' tag stopping `org-brain-visualize' from fetching entry text.
Only applies to headline entries."
:group 'org-brain
:type '(string))
(defcustom org-brain-exclude-resouces-tag "resourceless"
"`org-mode' tag stopping `org-brain-visualize' from fetching entry resources.
Only applies to headline entries."
:group 'org-brain
:type '(string))
(defcustom org-brain-exclude-children-tag "childless"
"`org-mode' tag which exclude the headline's children from org-brain's entries."
:group 'org-brain
:type '(string))
(defcustom org-brain-show-children-tag "showchildren"
"`org-mode' tag which get entire subtree from headline entry during `org-brain-text'."
:group 'org-brain
:type '(string))
(defcustom org-brain-exclude-tree-tag "nobrain"
"`org-mode' tag which exclude the headline and its children from org-brain's entries."
:group 'org-brain
:type '(string))
(defcustom org-brain-exclude-siblings-tag "nosiblings"
"`org-mode' tag which prevents the siblings of children of this node from being displayed."
:group 'org-brain
:type '(string))
(defcustom org-brain-exclude-local-parent-tag "nolocalparent"
"`org-mode' tag which prevents this node to be displayed as a local parent."
:group 'org-brain
:type '(string))
(defcustom org-brain-each-child-on-own-line-tag "ownline"
"`org-mode' tag which makes each child of the headline entry be listed on its own line."
:group 'org-brain
:type '(string))
(defcustom org-brain-no-sort-children-tag "nosort"
"`org-mode' tag which makes the children of the headline entry appear in file order rather than sorted."
:group 'org-brain
:type '(string))
(defcustom org-brain-wander-interval 3
"Seconds between randomized entries, when using `org-brain-visualize-wander'."
:group 'org-brain
:type 'integer)
(defcustom org-brain-title-max-length 0
"If a title is longer than this, it'll be capped during `org-brain-visualize'.
If 0 or a negative value, the title won't be capped."
:group 'org-brain
:type 'integer)
(defcustom org-brain-cap-mind-map-titles nil
"Whether to cap entries longer than org-brain-title-max-length in mind map visualization mode."
:group 'org-brain
:type '(boolean))
(defcustom org-brain-entry-separator ";"
"Can be used as a separator when adding children, parents, or friends.
Doing so allows for adding multiple entries at once."
:group 'org-brain
:type '(string))
(make-obsolete-variable
'org-brain-visualize-one-child-per-line
"Setting `org-brain-child-linebreak-sexp' to 0 visualizes one child per line."
"0.7")
(defcustom org-brain-child-linebreak-sexp 'fill-column
"Where to break lines when visualizing children?
Reasonable values include:
'0: every child will be on its own line
'fill-column: lines will break at `fill-column'
'(window-width): lines will break at the width of the window
'most-positive-fixnum: All children will be on one line"
:group 'org-brain
:type '(sexp))
(defcustom org-brain-refile-max-level 1
"The default max-level used by `org-brain-refile'."
:group 'org-brain
:type 'integer)
(defcustom org-brain-child-link-name "brain-child"
"The name for `org-mode' links, creating child relationships.
Must be set before `org-brain' is loaded.
Insert links using `org-insert-link'."
:group 'org-brain
:type '(string))
(defcustom org-brain-parent-link-name "brain-parent"
"The name for `org-mode' links, creating parent relationships.
Must be set before `org-brain' is loaded.
Insert links using `org-insert-link'."
:group 'org-brain
:type '(string))
(defcustom org-brain-friend-link-name "brain-friend"
"The name for `org-mode' links, creating friend relationships.
Must be set before `org-brain' is loaded.
Insert links using `org-insert-link'."
:group 'org-brain
:type '(string))
(defcustom org-brain-children-property-name "BRAIN_CHILDREN"
"The name for the org-mode property in which child relationships are stored.
Must be set before `org-brain' is loaded."
:group 'org-brain
:type '(string))
(defcustom org-brain-parents-property-name "BRAIN_PARENTS"
"The name for the org-mode property in which brain relationships are stored.
Must be set before `org-brain' is loaded."
:group 'org-brain
:type '(string))
(defcustom org-brain-friends-property-name "BRAIN_FRIENDS"
"The name for the org-mode property in which friend relationships are stored.
Must be set before `org-brain' is loaded."
:group 'org-brain
:type '(string))
(defcustom org-brain-edge-property-prefix-name "BRAIN_EDGE"
"The prefix for the org-mode property in which edge annotations are stored.
Must be set before `org-brain' is loaded."
:group 'org-brain
:type '(string))
(defcustom org-brain-resources-drawer-name "RESOURCES"
"The org-mode drawer name in which resources of an entry are stored.
Must be set before `org-brain' is loaded."
:group 'org-brain
:type '(string))
(defcustom org-brain-open-same-window nil
"Should `org-brain-visualize' open up in the same window it was launched in?"
:group 'org-brain
:type '(boolean))
(defcustom org-brain-completion-system 'default
"The completion system to be used by `org-brain'."
:group 'org-brain
:type '(radio
(const :tag "Ido" ido)
(const :tag "Helm" helm)
(const :tag "Ivy" ivy)
(const :tag "Default" default)
(function :tag "Custom function")))
;;;;; Faces and face helper functions
(defface org-brain-title
'((t . (:inherit 'org-level-1)))
"Face for the currently selected entry.")
(defface org-brain-wires
`((t . (:inherit 'font-lock-comment-face :italic nil)))
"Face for the wires connecting entries.")
(defface org-brain-button
'((t . (:inherit button)))
"Face for header-entry buttons in the org-brain visualize buffer.
File entries also use this, but also applies `org-brain-file-face-template'.")
(defface org-brain-parent
'((t . (:inherit (font-lock-builtin-face org-brain-button))))
"Face for the entries' linked header-entry parent nodes.
File entries also use this, but also applies `org-brain-file-face-template'.")
(defface org-brain-local-parent
'((t . (:inherit org-brain-parent :weight bold)))
"Face for the entries' local header-entry parent nodes.
File entries also use this, but also applies `org-brain-file-face-template'.")
(defface org-brain-child
'((t . (:inherit org-brain-button)))
"Face for the entries' linked header-entry child nodes.
File entries also use this, but also applies `org-brain-file-face-template'.")
(defface org-brain-local-child
'((t . (:inherit org-brain-child :weight bold)))
"Face for the entries' local header-entry child nodes.
File entries also use this, but also applies `org-brain-file-face-template'.")
(defface org-brain-sibling
'((t . (:inherit org-brain-child)))
"Face for the entries' header-entry sibling nodes.
File entries also use this, but also applies `org-brain-file-face-template'.")
(defface org-brain-local-sibling
'((t . (:inherit org-brain-sibling :weight bold)))
"Face for the entries' local header-entry sibling nodes.
An entry is a local sibling of another entry if they share a local parent.
File entries also use this, but also applies `org-brain-file-face-template'.")
(defface org-brain-friend
'((t . (:inherit org-brain-button)))
"Face for the entries' header-entry friend nodes.
File entries also use this, but also applies `org-brain-file-face-template'.")
(defface org-brain-pinned
'((t . (:inherit org-brain-button)))
"Face for pinned header entries.
File entries also use this, but also applies `org-brain-file-face-template'.")
(defface org-brain-selected-list
'((t . (:inherit org-brain-pinned)))
"Face for header entries in the selection list.
File entries also use this, but also applies `org-brain-file-face-template'.")
(defface org-brain-history-list
'((t . (:inherit org-brain-pinned)))
"Face for header entries in the history list.
File entries also use this, but also applies `org-brain-file-face-template'.")
(defface org-brain-file-face-template
'((t . (:slant italic)))
"Attributes of this face are added to file-entry faces.")
(defface org-brain-edge-annotation-face-template
'((t . (:box t)))
"Attributes of this face are added to links which have an edge annotation
to the visualized entry.")
;; This needs to be here or defface complains that it is undefined.
(defun org-brain-specified-face-attrs (face &optional frame)
"Return a plist of all face attributes of FACE that are not `unspecified'.
If FRAME is not specified, `selected-frame' is used."
(cl-labels ((alist->plist (alist)
(pcase alist
('nil nil)
(`((,h1 . ,h2) . ,tail) `(,h1 . (,h2 . ,(alist->plist tail)))))))
(alist->plist (seq-filter
(lambda (f) (not (equal (cdr f) 'unspecified)))
(face-all-attributes face (or frame (selected-frame)))))))
(defun org-brain-display-face (entry &optional face edge)
"Return the final display face for ENTRY.
Takes FACE as a starting face, or `org-brain-button' if FACE is not specified.
Applies the attributes in `org-brain-edge-annotation-face-template',
`org-brain-selected-face-template', and `org-brain-file-face-template'
as appropriate.
EDGE determines if `org-brain-edge-annotation-face-template' should be used."
(let ((selected-face-attrs
(when (member entry org-brain-selected)
(org-brain-specified-face-attrs 'org-brain-selected-face-template)))
(file-face-attrs
(when (org-brain-filep entry)
(org-brain-specified-face-attrs 'org-brain-file-face-template))))
(append (list :inherit (or face 'org-brain-button))
selected-face-attrs
file-face-attrs
(when edge
(org-brain-specified-face-attrs 'org-brain-edge-annotation-face-template)))))
(defface org-brain-selected-face-template
`((t . ,(org-brain-specified-face-attrs 'highlight)))
"Attributes of this face are added to the faces of selected entries.")
;;;; API
;; An entry is either a string or a list of three strings.
;; If a string, then the entry is a file.
;; If a list, then the entry is a headline:
;; ("file entry" "headline title" "ID")
;; There's also a special entry type: Nicknames
;; In the case of headline nicknames the car of the list is a symbol (instead of a string)
;; ('alias "headline title" "ID")
(defvar org-brain--vis-entry nil
"The last entry argument to `org-brain-visualize'.")
(defvar org-brain--vis-entry-keywords nil
"The `org-brain-keywords' of `org-brain--vis-entry'.")
(defvar org-brain--vis-history nil
"History previously visualized entries. Newest first.")
(defvar org-brain-resources-start-re (concat "^[ \t]*:" org-brain-resources-drawer-name ":[ \t]*$")
"Regular expression matching the first line of a resources drawer.")
(defvar org-brain-keyword-regex "^#\\+[a-zA-Z_]+:"
"Regular expression matching org keywords.")
(defvar org-brain-pins nil "List of pinned org-brain entries.")
(defvar org-brain-selected nil "List of selected org-brain entries.")
(defvar org-brain-headline-cache (make-hash-table :test 'equal)
"Cache for headline entries. Updates when files have been saved.")
;;;###autoload
(defun org-brain-update-id-locations ()
"Scan `org-brain-files' using `org-id-update-id-locations'."
(interactive)
(org-id-update-id-locations (org-brain-files)))
;;;###autoload
(defun org-brain-get-id ()
"Get ID of headline at point, creating one if it doesn't exist.
Run `org-brain-new-entry-hook' if a new ID is created."
(interactive)
(or (org-id-get)
(progn
(run-hooks 'org-brain-new-entry-hook)
(org-id-get nil t))))
;;;###autoload
(defun org-brain-switch-brain (directory)
"Choose another DIRECTORY to be your `org-brain-path'."
(interactive "D")
(if (file-equal-p directory org-brain-path)
(message "Current brain already is %s, no switch" directory)
(setq org-brain-path directory)
(setq org-brain-data-file (file-truename (expand-file-name ".org-brain-data.el" org-brain-path)))
(unless (file-exists-p org-brain-data-file)
(org-brain-save-data))
(setq org-brain-pins nil)
(setq org-brain--vis-history nil)
(load org-brain-data-file t)
(org-brain-update-id-locations)
(message "Switched org-brain to %s" directory)))
(defun org-brain-maybe-switch-brain ()
"Switch brain to `default-directory' if a file named \".org-brain-data.el\" exists there."
(when (and (not (file-equal-p default-directory org-brain-path))
(file-exists-p (file-truename (expand-file-name ".org-brain-data.el" default-directory))))
(org-brain-switch-brain default-directory)))
(defun org-brain-filep (entry)
"Return t if the ENTRY is a (potential) brain file."
(stringp entry))
(defun org-brain-save-data ()
"Save data to `org-brain-data-file'."
;; Code adapted from Magnar Sveen's multiple-cursors
(with-temp-file org-brain-data-file
(emacs-lisp-mode)
(dolist (data '(org-brain-pins))
(insert "(setq " (symbol-name data) "\n"
" '(")
(newline-and-indent)
(mapc #'(lambda (value)
(insert (format "%S" value))
(newline-and-indent))
(symbol-value data))
(insert "))")
(newline))))
(defun org-brain-path-entry-name (path)
"Get PATH as an org-brain entry name."
(string-remove-suffix (concat "." org-brain-files-extension)
(file-relative-name (file-truename path)
(file-truename org-brain-path))))
(defun org-brain-entry-path (entry &optional check-title)
"Get path of org-brain ENTRY.
If CHECK-TITLE is non-nil, consider that ENTRY might be a file entry title."
(let ((name (if (org-brain-filep entry)
(or (and check-title
org-brain-file-entries-use-title
(cdr
(assoc entry
(mapcar (lambda (x)
(cons (concat (file-name-directory x)
(org-brain-title x))
x))
(org-brain-files t)))))
entry)
(car entry))))
(file-truename (expand-file-name (org-link-unescape (format "%s.%s" name org-brain-files-extension))
org-brain-path))))
(defun org-brain-files (&optional relative)
"Get all org files (recursively) in `org-brain-path'.
If RELATIVE is t, then return relative paths and remove file extension.
Ignores \"dotfiles\"."
(make-directory org-brain-path t)
(if relative
(mapcar #'org-brain-path-entry-name (org-brain-files))
(if org-brain-scan-directories-recursively
(directory-files-recursively
org-brain-path (format "^[^.].*\\.%s$" org-brain-files-extension))
(directory-files
org-brain-path t (format "^[^.].*\\.%s$" org-brain-files-extension)))))
(defvar org-brain-link-re
"\\[\\[\\(\\(?:[^][\\]\\|\\\\\\(?:\\\\\\\\\\)*[][]\\|\\\\+[^][]\\)+\\)]\\(?:\\[\\(\\(?:.\\|\\)+?\\)]\\)?]"
"Regex matching an `org-mode' link.
The first match is the URI, the second is the (optional) desciption.
This variable should be the same as `org-link-bracket-re'.
However the implementation changed in `org-mode' 9.3 and
the old `org-bracket-link-regexp' had different match groups.
The purpose of `org-brain-link-re' is protection against future changes.")
(defun org-brain-replace-links-with-visible-parts (raw-str)
"Get RAW-STR with its links replaced by their descriptions."
(let ((ret-str "")
(start 0)
match-start)
(while (setq match-start (string-match org-brain-link-re raw-str start))
(setq ret-str
(concat ret-str
;; Include everything not part of the string.
(substring-no-properties raw-str start match-start)
;; Include either the link description, or the link
;; destination.
(or (match-string-no-properties 2 raw-str)
(match-string-no-properties 1 raw-str))))
(setq start (match-end 0)))
(concat ret-str (substring-no-properties raw-str start nil))))
(defun org-brain-headline-at (&optional pom)
"Return the full headline of the entry at POM.
If `org-brain-headline-links-only-show-visible' is nil, the links
will be returned raw (all of the bracket syntax visible.)
If `org-brain-headline-links-only-show-visible' is non-nil,
returns only the visible parts of links in the heading. (For any
links that have descriptions, only the descriptions will be
returned.)
This is done via regex, and does not depend on org-mode's
visibility rendering/formatting in-buffer."
(let ((pom (or pom (point))))
(if org-brain-headline-links-only-show-visible
(org-brain-replace-links-with-visible-parts (org-entry-get pom "ITEM"))
(org-entry-get pom "ITEM"))))
(defun org-brain--headline-entry-at-point (&optional create-id)
"Get headline entry at point.
If CREATE-ID is non-nil, call `org-brain-get-id' first."
(if create-id (org-brain-get-id))
(when-let ((id (org-entry-get (point) "ID")))
(list (org-brain-path-entry-name buffer-file-name)
(org-brain-headline-at (point)) id)))
(defun org-brain-entry-at-point-excludedp ()
"Return t if the entry at point is tagged as being excluded from org-brain."
(let ((tags (org-get-tags)))
(or (member org-brain-exclude-tree-tag tags)
(and (member org-brain-exclude-children-tag tags)
(not (member org-brain-exclude-children-tag
(org-get-tags nil t)))))))
(defun org-brain-id-exclude-taggedp (id)
"Return t if ID is tagged as being excluded from org-brain."
(org-with-point-at (org-id-find id t)
(org-brain-entry-at-point-excludedp)))
(defun org-brain--name-and-id-at-point ()
"Get name and id of headline entry at point.
Respect excluded entries."
(unless (org-brain-entry-at-point-excludedp)
(when-let ((id (org-entry-get (point) "ID")))
(list (org-brain-headline-at (point)) id))))
(defun org-brain--nicknames-at-point ()
"Get nicknames of the headline entry at point."
(when-let ((id (org-entry-get (point) "ID")))
(mapcar (lambda (nickname)
(list 'nickname nickname id))
(org-entry-get-multivalued-property (point) "NICKNAMES"))))
(defun org-brain-headline-entries-in-file (file &optional no-temp-buffer)
"Get a list of all headline (and nicknames) entries in FILE.
If the entries are cached in `org-brain-headline-cache', get them from there.
Else the FILE is inserted in a temp buffer and get scanned for entries.
If NO-TEMP-BUFFER is non-nil, run the scanning in the current buffer instead."
(if no-temp-buffer
(let ((cached (gethash file org-brain-headline-cache nil)))
(if (or (not cached)
(not (equal (car cached)
(file-attribute-modification-time
(file-attributes file)))))
(let ((file-entry (org-brain-path-entry-name file)))
(insert-file-contents file nil nil nil 'replace)
(cdr (puthash file (cons (file-attribute-modification-time
(file-attributes file))
(apply #'append
(mapcar (lambda (entry) (cons file-entry entry))
(remove nil (org-map-entries
#'org-brain--name-and-id-at-point)))
(remove nil (org-map-entries #'org-brain--nicknames-at-point))))
org-brain-headline-cache)))
(cdr cached)))
(with-temp-buffer
(delay-mode-hooks
(org-mode)
(org-brain-headline-entries-in-file file t)))))
(defun org-brain-headline-entries (&optional include-nicknames)
"Get all org-brain headline entries.
INCLUDE-NICKNAMES also return duplicates for headlines with NICKNAMES property."
(with-temp-buffer
(delay-mode-hooks
(org-mode)
(apply #'append
(mapcar
(lambda (file)
(seq-filter
(if include-nicknames
#'identity
(lambda (x) (stringp (car x))))
(org-brain-headline-entries-in-file file t)))
(org-brain-files))))))
(defun org-brain-entry-from-id (id)
"Get entry from ID."
(unless org-id-locations (org-id-locations-load))
(when-let ((path (gethash id org-id-locations)))
(list (org-brain-path-entry-name path)
(org-brain-headline-at (org-id-find id t))
id)))
(defun org-brain-entry-identifier (entry)
"Get identifier of ENTRY.
The identifier is an id if ENTRY is a headline.
If ENTRY is file, then the identifier is the relative file name."
(if (org-brain-filep entry)
(org-entry-protect-space entry)
(nth 2 entry)))
(defun org-brain-entry-at-pt (&optional create-id)
"Get current org-brain entry.
CREATE-ID asks to create an ID öif there isn't one already."
(cond ((eq major-mode 'org-mode)
(unless (string-prefix-p (file-truename org-brain-path)
(file-truename (buffer-file-name)))
(error "Not in a brain file"))
(if org-brain-scan-for-header-entries
(if (ignore-errors (org-get-heading))
(or (org-brain--headline-entry-at-point)
(when create-id
(let ((closest-parent
(save-excursion
(let ((e))
(while (and (not e) (org-up-heading-safe))
(setq e (org-brain--headline-entry-at-point)))
(or e
(when org-brain-include-file-entries
(org-brain-path-entry-name (buffer-file-name))))))))
(if (y-or-n-p
(format "'%s' has no ID, create one%s? "
(org-brain-headline-at)
(if closest-parent
(format " [else use local parent '%s']"
(org-brain-title closest-parent))
"")))
(org-brain--headline-entry-at-point t)
(or (org-brain-entry-at-pt) (error "No entry at pt"))))))
(if org-brain-include-file-entries
(org-brain-path-entry-name (buffer-file-name))
(error "Not under an org headline, and org-brain-include-file-entries is nil")))
(org-brain-path-entry-name (buffer-file-name))))
((eq major-mode 'org-brain-visualize-mode)
org-brain--vis-entry)
(t
(error "Not in org-mode or org-brain-visualize"))))
(defun org-brain-entry-name (entry)
"Get name string of ENTRY."
(if (org-brain-filep entry)
(if org-brain-file-entries-use-title
(concat (file-name-directory entry) (org-brain-title entry))
entry)
(format org-brain-headline-entry-name-format-string
(org-brain-entry-name (car entry)) (cadr entry))))
(defun org-brain-entry-data (entry)
"Run `org-element-parse-buffer' on ENTRY text."
(with-temp-buffer
(insert (org-brain-text entry t))
(org-element-parse-buffer)))
(defun org-brain--file-targets (file)
"Return alist of (name . entry-id) for all entries in FILE.
The list also includes nicknames from the NICKNAMES keyword/properties.
Should only be used in a temp-buffer."
(let* ((file-relative (org-brain-path-entry-name file))
(file-entry-name (org-brain-entry-name file-relative)))
(remove
nil
(append
(when org-brain-include-file-entries
(apply
#'append
(list (cons file-entry-name file-relative))
(mapcar (lambda (x)
(list (cons (org-entry-restore-space x) file-relative)))
(when-let ((nicknames (assoc "NICKNAMES" (org-brain-keywords file-relative))))
(split-string (cdr nicknames) " " t)))))
(mapcar
(lambda (x)
(cons (format org-brain-headline-entry-name-format-string
file-entry-name
(nth 1 x))
(nth 2 x)))
(org-brain-headline-entries-in-file file t))))))
(defun org-brain--all-targets ()
"Get an alist with (name . entry-id) of all targets in org-brain.
`org-brain-include-file-entries' and `org-brain-scan-for-header-entries'
affect the fetched targets."
(if org-brain-scan-for-header-entries
(with-temp-buffer
(delay-mode-hooks
(org-mode)
(mapcan #'org-brain--file-targets
(org-brain-files))))
(mapcar (lambda (x) (cons (org-brain-entry-name x) x))
(org-brain-files t))))
(defun org-brain-completing-read (prompt choices &optional predicate require-match initial-input hist def inherit-input)
"A version of `completing-read' which is tailored to `org-brain-completion-system'."
(let ((args (list prompt choices predicate require-match initial-input hist def inherit-input)))
(or (pcase org-brain-completion-system
('default (apply #'completing-read args))
('ido (apply #'ido-completing-read args))
('ivy (apply #'ivy-completing-read args))
('helm (apply #'helm-completing-read-default-1
(append args '("org-brain" "*org-brain-helm*")))))
(funcall org-brain-completion-system prompt choices))))
(defun org-brain-get-entry-from-title (title &optional targets)
"Search for TITLE in TARGETS and return an entry. Create it if non-existing.
TARGETS is an alist of (title . entry-id).
If TARGETS is nil then use `org-brain--all-targets'."
(unless org-id-locations (org-id-locations-load))
(let* ((targets (or targets (org-brain--all-targets)))
(id (or (cdr (assoc title targets)) title)))
(or
;; Headline entry exists, return it
(org-brain-entry-from-id id)
;; File entry
(progn
(setq id (split-string id "::" t))
(let* ((entry-path (org-brain-entry-path (car id) t))
(entry-file (org-brain-path-entry-name entry-path)))
(unless (file-exists-p entry-path)
(if (and org-brain-default-file-parent (equal (length id) 1))
(setq entry-file org-brain-default-file-parent
id `(,org-brain-default-file-parent ,(car id)))
(make-directory (file-name-directory entry-path) t)
(write-region "" nil entry-path)))
(if (or (not org-brain-include-file-entries)
(equal (length id) 2)
(not (equal (car id) entry-file)))
;; Create new headline entry in file
(org-with-point-at (org-brain-entry-marker entry-file)
(if (and (not org-brain-include-file-entries)
(or
;; Search heading without tags
(save-excursion
(re-search-forward (concat "\n\\* +" (regexp-quote (car id)) "[ \t]*$") nil t))
;; Search heading with tags
(save-excursion
(re-search-forward (concat "\n\\* +" (regexp-quote (car id)) "[ \t]+:.*:$") nil t))))
(org-brain-entry-at-pt)
(goto-char (point-max))
(insert (concat "\n* " (or (cadr id) (car id))))
(let ((new-id (org-brain-get-id)))
(save-buffer)
(list entry-file (or (cadr id) (car id)) new-id))))
entry-file))))))
;;;###autoload
(defun org-brain-add-entry (title)
"Add a new entry named TITLE."
(interactive "sNew entry: ")
(message "Added new entry: '%s'"
(org-brain-entry-name (org-brain-get-entry-from-title title))))
(defun org-brain-choose-entries (prompt entries &optional predicate require-match initial-input hist def inherit-input-method)
"PROMPT for one or more ENTRIES, separated by `org-brain-entry-separator'.
ENTRIES can be a list, or 'all which lists all headline and file entries.
Return the prompted entries in a list.
Very similar to `org-brain-choose-entry', but can return several entries.
For PREDICATE, REQUIRE-MATCH, INITIAL-INPUT, HIST, DEF and
INHERIT-INPUT-METHOD see `completing-read'."
(let* ((targets (if (eq entries 'all)
(org-brain--all-targets)
(mapcar (lambda (x)
(cons (org-brain-entry-name x)
(if (org-brain-filep x)
x
(nth 2 x))))
entries)))
(choices (org-brain-completing-read prompt targets
predicate require-match initial-input hist def inherit-input-method)))
(mapcar (lambda (title) (org-brain-get-entry-from-title title targets))
(if org-brain-entry-separator
(split-string choices org-brain-entry-separator)
(list choices)))))
(defun org-brain-choose-entry (prompt entries &optional predicate require-match initial-input hist def inherit-input-method)
"PROMPT for an entry from ENTRIES and return it.
ENTRIES can be 'all, which lists all headline and file entries.
For PREDICATE, REQUIRE-MATCH, INITIAL-INPUT, HIST, DEF and INHERIT-INPUT-METHOD see `completing-read'."
(let ((org-brain-entry-separator nil))
(car (org-brain-choose-entries prompt entries predicate require-match initial-input hist def inherit-input-method))))
(defun org-brain-first-headline-position ()
"Get position of first headline in buffer. `point-max' if no headline exists."
(save-excursion
(goto-char (point-min))
(or (looking-at-p org-heading-regexp)
(outline-next-heading)
(goto-char (point-max)))
(point)))
(defun org-brain-keywords (entry)
"Get alist of `org-mode' keywords and their values in file ENTRY."
(if (org-brain-filep entry)
(with-temp-buffer
(insert
(with-temp-buffer
(ignore-errors (insert-file-contents (org-brain-entry-path entry)))
(buffer-substring-no-properties (point-min) (org-brain-first-headline-position))))