1 ;;; org-contacts.el --- Contacts management system for Org mode -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2010-2022 Free Software Foundation, Inc.
5 ;; Author: Julien Danjou <julien@danjou.info>
6 ;; Maintainer: stardiviner <numbchild@gmail.com>
7 ;; Keywords: contacts, org-mode, outlines, hypermedia, calendar
9 ;; Package-Requires: ((emacs "29.1") (org "9.7"))
10 ;; Homepage: https://repo.or.cz/org-contacts.git
12 ;; This file is not part of GNU Emacs.
14 ;; This program is free software: you can redistribute it and/or modify
15 ;; it under the terms of the GNU General Public License as published by
16 ;; the Free Software Foundation, either version 3 of the License, or
17 ;; (at your option) any later version.
19 ;; This program is distributed in the hope that it will be useful,
20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 ;; GNU General Public License for more details.
24 ;; You should have received a copy of the GNU General Public License
25 ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
26 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
30 ;; This file contains the code for managing your contacts into Org-mode.
32 ;; To enter new contacts, you can use `org-capture' and a minimal template just like
35 ;; ("c" "Contacts" entry (file "~/Org/contacts.org")
36 ;; "* %(org-contacts-template-name)
38 ;; :EMAIL: %(org-contacts-template-email)
41 ;; You can also use a complex template, for example:
43 ;; ("c" "Contacts" entry (file "~/Org/contacts.org")
44 ;; "* %(org-contacts-template-name)
46 ;; :EMAIL: %(org-contacts-template-email)
60 ;; - You can use [M-x org-contacts] command to search.
62 ;; - You can use `org-sparse-tree' [C-c / p] to filter based on a
63 ;; specific property. Or other matcher on `org-sparse-tree'.
73 (require 'org-capture
)
75 (autoload 'with-memoization
"subr")
77 (declare-function diary-anniversary
"diary-lib" (month day
&optional year mark
))
78 (declare-function erc-server-buffer-live-p
"erc" ())
79 (declare-function erc-server-process-alive
"erc" (&optional buffer
))
80 (defvar erc-server-processing-p
)
82 (defgroup org-contacts nil
83 "Options about contacts management."
86 (defcustom org-contacts-directory nil
87 "Directory of Org files to use as contacts source.
88 When set to nil, all your Org files will be used."
91 (defcustom org-contacts-files nil
92 "List of Org files to use as contacts source.
93 When set to nil, all your Org files will be used."
96 (defcustom org-contacts-email-property
"EMAIL"
97 "Name of the property for contact email address."
100 (defcustom org-contacts-tel-property
"PHONE"
101 "Name of the property for contact phone number."
104 (defcustom org-contacts-address-property
"ADDRESS"
105 "Name of the property for contact address."
108 (defcustom org-contacts-birthday-property
"BIRTHDAY"
109 "Name of the property for contact birthday date."
112 (defcustom org-contacts-note-property
"NOTE"
113 "Name of the property for contact note."
116 (defcustom org-contacts-alias-property
"ALIAS"
117 "Name of the property for contact name alias."
120 (defcustom org-contacts-ignore-property
"IGNORE"
121 "Name of the property which values ignored when completing or exporting to vCard."
124 (defcustom org-contacts-birthday-format
"Birthday: %l (%Y)"
125 "Format of the anniversary agenda entry.
126 The following replacements are available:
129 %l - Link to the heading
131 %Y - Number of year (ordinal)"
134 (defcustom org-contacts-last-read-mail-property
"LAST_READ_MAIL"
135 "Name of the property for contact last read email link storage."
138 (defcustom org-contacts-icon-property
"ICON"
139 "Name of the property for contact icon."
142 (defcustom org-contacts-nickname-property
"NICKNAME"
143 "Name of the property for IRC nickname match."
146 (defcustom org-contacts-icon-size
64
147 "Size of the contacts icons."
150 (defcustom org-contacts-icon-use-gravatar
(fboundp 'gravatar-retrieve
)
151 "Whether use Gravatar to fetch contact icons."
154 (defcustom org-contacts-completion-ignore-case t
155 "Ignore case when completing contacts."
158 (defcustom org-contacts-group-prefix
"+"
162 (defcustom org-contacts-tags-props-prefix
"#"
163 "Tags and properties prefix."
166 (defcustom org-contacts-matcher
167 (mapconcat #'identity
168 (mapcar (lambda (x) (concat x
"<>\"\""))
169 (list org-contacts-email-property
170 org-contacts-alias-property
171 org-contacts-tel-property
172 org-contacts-address-property
173 org-contacts-birthday-property
))
175 "Matching rule for finding heading that are contacts.
176 This can be a tag name, or a property check."
179 (defcustom org-contacts-email-link-description-format
"%s (%d)"
180 "Format used to store links to email.
181 This overrides `org-email-link-description-format' if set."
184 (defcustom org-contacts-vcard-file
"contacts.vcf"
185 "Default file for vcard export."
188 (defcustom org-contacts-completion-enabled-mode-list
'(org-mode message-mode mu4e-compose-mode
)
189 "Enable or not the completion in `message-mode' with `org-contacts'."
190 :type
'(repeat symbol
))
192 (defcustom org-contacts-complete-functions
193 '(org-contacts-complete-group org-contacts-complete-tags-props org-contacts-complete-name
)
194 "List of functions used to complete contacts in `message-mode'."
197 ;; Decalre external functions and variables
198 (declare-function org-reverse-string
"org")
199 (declare-function diary-ordinal-suffix
"ext:diary-lib")
200 (declare-function wl-summary-message-number
"ext:wl-summary")
201 (declare-function wl-address-header-extract-address
"ext:wl-address")
202 (declare-function wl-address-header-extract-realname
"ext:wl-address")
203 (declare-function erc-buffer-list
"ext:erc")
204 (declare-function erc-get-channel-user-list
"ext:erc")
205 (declare-function google-maps-static-show
"ext:google-maps-static")
206 (declare-function elmo-message-field
"ext:elmo-pipe")
207 (declare-function std11-narrow-to-header
"ext:std11")
208 (declare-function std11-fetch-field
"ext:std11")
211 (defun org-contacts-files ()
212 "Return list of Org files to use for contact management."
213 (if org-contacts-files
215 (message "[org-contacts] ERROR: Your custom variable `org-contacts-files' is nil.
216 Revert to `org-agenda-files' now.")
217 (org-agenda-files t
'ifmode
)))
219 (defconst org-contacts-property-values-separators
"[,; \f\t\n\r\v]+"
220 "The default value of separators for `org-contacts-split-property'.
222 A regexp matching strings of whitespace, `,' and `;'.")
224 (defvar org-contacts-keymap
225 (let ((map (make-sparse-keymap)))
226 (define-key map
"M" #'org-contacts-view-send-email
)
227 (define-key map
"i" #'org-contacts-view-switch-to-irc-buffer
)
229 "The keymap used in `org-contacts' result list.")
231 (defvar org-contacts-db nil
232 "Org Contacts database.")
234 (defvar org-contacts-last-update nil
235 "Last time the Org Contacts database has been updated.")
237 (defvar org-contacts-all-contacts nil
238 "A data store variable of all contacts.")
240 (defun org-contacts-all-contacts ()
241 "Return the data of all contacts."
242 (setq org-contacts-all-contacts
243 (with-memoization org-contacts-all-contacts
244 (org-contacts--all-contacts))))
246 (defun org-contacts-db-need-update-p ()
247 "Determine whether `org-contacts-db' needs to be refreshed."
248 (or (null org-contacts-last-update
)
249 (cl-find-if (lambda (file)
250 (or (time-less-p org-contacts-last-update
251 (elt (file-attributes file
) 5))))
252 (org-contacts-files))
253 (org-contacts-db-has-dead-markers-p org-contacts-db
)))
255 (defun org-contacts-db-has-dead-markers-p (db)
256 "Return t if at least one dead marker is found in DB.
257 A dead marker in this case is a marker pointing to dead or no
259 ;; Scan contacts list looking for dead markers, and return t at first found.
260 (catch 'dead-marker-found
262 (unless (marker-buffer (nth 1 (car db
)))
263 (throw 'dead-marker-found t
))
267 (defun org-contacts-db ()
268 "Return the latest Org Contacts Database."
269 (let* ((org--matcher-tags-todo-only nil
)
270 (contacts-matcher (cdr (org-make-tags-matcher org-contacts-matcher
)))
271 result org-agenda-skip-function org-agenda-skip-function-global
)
272 (when (org-contacts-db-need-update-p)
273 (let ((progress-reporter
274 (make-progress-reporter "Updating Org Contacts Database..." 0 (length (org-contacts-files))))
276 (dolist (file (org-contacts-files))
278 ;; if file doesn't exist and the user agrees to removing it
279 ;; from org-agendas-list, 'nextfile is thrown. Catch it here
280 ;; and skip processing the file.
282 ;; TODO: suppose that the user has set an org-contacts-files
283 ;; list that contains an element that doesn't exist in the
284 ;; file system: in that case, the org-agenda-files list could
285 ;; be updated (and saved to the customizations of the user) if
286 ;; it contained the same file even though the org-agenda-files
287 ;; list wasn't actually used. I don't think it is normal that
288 ;; org-contacts updates org-agenda-files in this case, but
289 ;; short of duplicating org-check-agenda-files and
290 ;; org-remove-files, I don't know how to avoid it.
292 ;; A side effect of the TODO is that the faulty
293 ;; org-contacts-files list never gets updated and thus the
294 ;; user is always queried about the missing files when
295 ;; org-contacts-db-need-update-p returns true.
296 (org-check-agenda-file file
))
297 (message "Skipped %s removed from org-agenda-files list."
298 (abbreviate-file-name file
))
299 (with-current-buffer (org-get-agenda-file-buffer file
)
300 (unless (eq major-mode
'org-mode
)
301 (error "File %s is not in `org-mode'" file
))
304 (org-scan-tags 'org-contacts-at-point
306 org--matcher-tags-todo-only
)))))
307 (progress-reporter-update progress-reporter
(setq i
(1+ i
))))
308 (setf org-contacts-db result
309 org-contacts-last-update
(current-time))
310 (progress-reporter-done progress-reporter
)))
313 (defun org-contacts-search-contact (name)
314 "Search contact NAME in cached database and return org element POM."
316 (dolist (contact (org-contacts-all-contacts) epom
)
317 (when (string-equal (plist-get contact
:name
) name
)
318 (with-current-buffer (find-file-noselect (expand-file-name (car org-contacts-files
)))
320 (goto-char (plist-get contact
:position
))
321 (setq epom
(org-element-context)))
323 (org-goto-marker-or-bmk (org-find-exact-headline-in-buffer name
))
324 (setq epom
(org-element-context)))))))
328 ;; (org-contacts-search-contact "stardiviner")
329 ;; (org-element-property :title (org-contacts-search-contact "stardiviner"))
331 (defun org-contacts-at-point (&optional pom
)
332 "Return the contacts at point or marker POM or current position."
333 (setq pom
(or pom
(point)))
334 (org-with-point-at pom
335 (list (org-get-heading t
) (set-marker (make-marker) pom
) (org-entry-properties pom
'all
))))
337 (defun org-contacts-filter (&optional name-match tags-match prop-match
)
338 "Search for a contact matching any of NAME-MATCH, TAGS-MATCH, PROP-MATCH.
339 If all match values are nil, return all contacts.
341 The optional PROP-MATCH argument is a single (PROP . VALUE) cons
342 cell corresponding to the contact properties."
343 (if (and (null name-match
)
347 (cl-loop for contact in
(org-contacts-db)
350 (string-match-p name-match
353 (cl-find-if (lambda (prop)
354 (and (string= (car prop-match
) (car prop
))
355 (string-match-p (cdr prop-match
) (cdr prop
))))
358 (cl-find-if (lambda (tag)
359 (string-match-p tags-match tag
))
361 (or (cdr (assoc-string "ALLTAGS" (caddr contact
))) "") ":"))))
364 (defun org-contacts-try-completion-prefix (to-match collection
&optional predicate
)
365 "Custom implementation of `try-completion'.
366 This version works only with list and alist and it looks at all
367 prefixes rather than just the beginning of the string."
368 (cl-loop with regexp
= (concat "\\b" (regexp-quote to-match
))
374 for string
= (if (listp el
) (car el
) el
)
376 for start
= (when (or (null predicate
) (funcall predicate string
))
377 (string-match regexp string
))
380 do
(let ((end (match-end 0))
381 (len (length string
)))
384 (cl-destructuring-bind (string start end
)
386 (cl-values string start end
)
387 (org-contacts-common-substring
388 ret ret-start ret-end
395 (replace-regexp-in-string "\\`[ \t\n]*" "" ret
))))
397 (defun org-contacts-compare-strings (s1 start1 end1 s2 start2 end2
&optional ignore-case
)
398 "Compare the contents of two strings, using `compare-strings'.
400 This function works like `compare-strings' excepted that it
402 - The CAR is the number of characters that match at the beginning.
403 - The CDR is T is the two strings are the same and NIL otherwise."
404 (let ((ret (compare-strings s1 start1 end1 s2 start2 end2 ignore-case
)))
406 (cons (or end1
(length s1
)) t
)
407 (cons (1- (abs ret
)) nil
))))
409 (defun org-contacts-common-substring (s1 start1 end1 s2 start2 end2
)
410 "Extract the common substring between S1 and S2.
412 This function extracts the common substring between S1 and S2 and
413 adjust the part that remains common.
415 START1 and END1 delimit the part in S1 that we know is common
416 between the two strings. This applies to START2 and END2 for S2.
418 This function returns a list whose contains:
419 - The common substring found.
420 - The new value of the start of the known inner substring.
421 - The new value of the end of the known inner substring."
422 ;; Given two strings:
424 ;; s2: "fooo bar baz"
425 ;; and the inner substring is "bar"
426 ;; then: start1 = 4, end1 = 6, start2 = 5, end2 = 7
428 ;; To find the common substring we will compare two substrings:
429 ;; " oof" and " ooof" to find the beginning of the common substring.
430 ;; " baz" and " baz" to find the end of the common substring.
431 (let* ((len1 (length s1
))
432 (start1 (or start1
0))
433 (end1 (or end1 len1
))
436 (start2 (or start2
0))
437 (end2 (or end2 len2
))
439 (new-start (car (org-contacts-compare-strings
440 (substring (org-reverse-string s1
) (- len1 start1
)) nil nil
441 (substring (org-reverse-string s2
) (- len2 start2
)) nil nil
)))
443 (new-end (+ end1
(car (org-contacts-compare-strings
444 (substring s1 end1
) nil nil
445 (substring s2 end2
) nil nil
)))))
446 (list (substring s1
(- start1 new-start
) new-end
)
448 (+ new-start
(- end1 start1
)))))
450 (defun org-contacts-all-completions-prefix (to-match collection
&optional predicate
)
451 "Custom version of `all-completions'.
452 This version works only with list and alist and it looks at all
453 prefixes rather than just the beginning of the string."
454 (cl-loop with regexp
= (concat "\\b" (regexp-quote to-match
))
456 for string
= (if (listp el
) (car el
) el
)
457 for match?
= (when (and (or (null predicate
) (funcall predicate string
)))
458 (string-match regexp string
))
461 (let ((end (match-end 0)))
462 (org-no-properties string
)
463 (when (< end
(length string
))
464 ;; Here we add a text property that will be used
465 ;; later to highlight the character right after
466 ;; the common part between each addresses.
467 ;; See `org-contacts-display-sort-function'.
468 (put-text-property end
(1+ end
) 'org-contacts-prefix
't string
)))
471 (defun org-contacts-make-collection-prefix (collection)
472 "Make a collection function from COLLECTION which will match on prefixes."
473 (let ((collection collection
))
474 (lambda (string predicate flag
)
476 (org-contacts-try-completion-prefix string collection predicate
))
478 ;; `org-contacts-all-completions-prefix' has already been
479 ;; used to compute `all-completions'.
482 (org-contacts-test-completion-prefix string collection predicate
))
483 ((and (listp flag
) (eq (car flag
) 'boundaries
))
484 (org-contacts-boundaries-prefix string collection predicate
(cdr flag
)))
486 (org-contacts-metadata-prefix))
487 (t nil
; operation unsupported
490 (defun org-contacts-display-sort-function (completions)
491 "Sort function for contacts COMPLETIONS."
492 (mapcar (lambda (string)
493 (cl-loop with len
= (1- (length string
))
494 for i upfrom
0 to len
495 if
(memq 'org-contacts-prefix
496 (text-properties-at i string
))
497 do
(set-text-properties
499 (list 'font-lock-face
500 (if (char-equal (aref string i
)
501 (string-to-char " "))
502 ;; Spaces can't be bold.
506 do
(set-text-properties i
(1+ i
) nil string
)
507 finally
(cl-return string
)))
510 (defun org-contacts-test-completion-prefix (string collection predicate
)
511 (cl-find-if (lambda (el)
512 (and (or (null predicate
) (funcall predicate el
))
513 (string= string el
)))
516 (defun org-contacts-boundaries-prefix (string collection predicate suffix
)
517 (cl-list* 'boundaries
(completion-boundaries string collection predicate suffix
)))
519 (defun org-contacts-metadata-prefix (&rest _
)
521 ((cycle-sort-function . org-contacts-display-sort-function
)
522 (display-sort-function . org-contacts-display-sort-function
))))
524 (defun org-contacts-complete-group (string)
525 "Complete STRING as start from a group.
527 A group FOO is composed of contacts with the tag FOO."
528 (let* ((completion-ignore-case org-contacts-completion-ignore-case
)
529 (group-completion-p (string-match-p
530 (concat "^" org-contacts-group-prefix
) string
)))
531 (when group-completion-p
532 (let ((completion-list
535 (mapcar (lambda (group)
536 (propertize (concat org-contacts-group-prefix group
)
537 'org-contacts-group group
))
539 (cl-loop for contact in
(org-contacts-filter)
540 nconc
(org-split-string
541 (or (cdr (assoc-string "ALLTAGS" (caddr contact
))) "") ":")))))))
543 (if (= (length completion-list
) 1)
544 ;; We've found the correct group, returns the address
545 (let ((tag (get-text-property 0 'org-contacts-group
546 (car completion-list
))))
547 (mapconcat #'identity
548 (cl-loop for contact in
(org-contacts-filter
551 ;; The contact name is always the car of the assoc-list
552 ;; returned by `org-contacts-filter'.
553 for contact-name
= (car contact
)
554 ;; Grab the first email of the contact
555 for email
= (org-contacts-strip-link
556 (or (car (org-contacts-split-property
558 (cdr (assoc-string org-contacts-email-property
561 ;; If the user has an email address, append USER <EMAIL>.
562 if email collect
(org-contacts-format-email contact-name email
))
564 ;; We haven't found the correct group
565 (completion-table-case-fold completion-list
566 (not org-contacts-completion-ignore-case
)))))))
568 (defun org-contacts-complete-tags-props (string)
569 "Insert emails that match the tags expression beginning with STRING.
571 For example: FOO-BAR will match entries tagged with FOO but not with BAR.
572 See (org) Matching tags and properties for a complete description."
573 (let* ((completion-ignore-case org-contacts-completion-ignore-case
)
574 (completion-p (string-match-p
575 (concat "^" org-contacts-tags-props-prefix
) string
)))
580 (cl-loop for contact in
(org-contacts-db)
581 for contact-name
= (car contact
)
582 for email
= (org-contacts-strip-link
583 (or (car (org-contacts-split-property
585 (cdr (assoc-string org-contacts-email-property
589 ;; for tags = (cdr (assoc "TAGS" (nth 2 contact)))
590 ;; for tags-list = (if tags
591 ;; (split-string (substring (cdr (assoc "TAGS" (nth 2 contact))) 1 -1) ":")
593 for marker
= (nth 1 contact
)
594 if
(with-current-buffer (marker-buffer marker
)
597 ;; FIXME: AFAIK, `org-make-tags-matcher' returns
598 ;; a cons whose cdr is a function, so why do we
599 ;; pass it to `eval'?
600 (eval (cdr (org-make-tags-matcher (cl-subseq string
1)))
602 collect
(org-contacts-format-email contact-name email
))
604 (when (not (string= "" result
))
607 (defun org-contacts-remove-ignored-property-values (ignore-list list
)
608 "Remove all IGNORE-LIST's elements from LIST and you can use regular expressions in the ignore list."
609 (cl-remove-if (lambda (el)
610 (cl-find-if (lambda (x)
611 (string-match-p x el
))
615 (defun org-contacts-complete-name (string)
616 "Complete STRING at start with a user name and email."
617 (let* ((completion-ignore-case org-contacts-completion-ignore-case
)
619 (cl-loop for contact in
(org-contacts-filter)
620 ;; The contact name is always the car of the assoc-list
621 ;; returned by `org-contacts-filter'.
622 for contact-name
= (car contact
)
624 ;; Build the list of the email addresses which has
626 for ignore-list
= (org-contacts-split-property
627 (or (cdr (assoc-string org-contacts-ignore-property
628 (nth 2 contact
))) ""))
629 ;; Build the list of the user email addresses.
630 for email-list
= (org-contacts-remove-ignored-property-values
632 (org-contacts-split-property
633 (or (cdr (assoc-string org-contacts-email-property
634 (nth 2 contact
))) "")))
635 ;; If the user has email addresses…
637 ;; … append a list of USER <EMAIL>.
638 nconc
(cl-loop for email in email-list
639 collect
(org-contacts-format-email
640 contact-name
(org-contacts-strip-link email
)))))
641 (completion-list (org-contacts-all-completions-prefix
643 (org-uniquify completion-list
))))
644 (when completion-list
645 (org-contacts-make-collection-prefix completion-list
))))
647 (defun org-contacts-message-complete-function ()
648 "Function used in `completion-at-point-functions' in `message-mode'."
649 (let ((mail-abbrev-mode-regexp
650 "^\\(Resent-To\\|To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\|Disposition-Notification-To\\|Return-Receipt-To\\):"))
651 (when (mail-abbrev-in-expansion-header-p)
655 (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
656 (goto-char (match-end 0))
661 (completion-table-dynamic
663 (run-hook-with-args-until-success
664 'org-contacts-complete-functions string
))))))))
666 (defun org-contacts-org-complete--annotation-function (candidate)
667 "Return `org-contacts' tags of contact CANDIDATE."
671 (defun org-contacts--candidates-org-complete-get-doc (candidate)
672 "Return `org-contacts' content of contact CANDIDATE."
673 (let* ((contact (seq-find
674 (lambda (contact) (string-equal (plist-get contact
:name
) (get-text-property 0 'contact-name candidate
)))
675 (org-contacts-all-contacts)))
676 (name (plist-get contact
:name
))
677 (file (plist-get contact
:file
))
678 (position (plist-get contact
:position
))
679 (org-contact-buffer (get-buffer (find-file-noselect file
)))
680 ;; get org-contact headline and property drawer.
681 (contact-content (with-current-buffer org-contact-buffer
682 (when (derived-mode-p 'org-mode
)
687 (org-narrow-to-block))
688 (t (org-narrow-to-subtree)))
689 (let ((content (buffer-substring-no-properties (point-min) (point-max))))
690 (when (buffer-narrowed-p) (widen))
692 (cons name contact-content
)))
695 ;; (setq org-contacts--candidates-complete-doc-cache nil)
696 ;; (org-contacts--candidates-complete-doc-cache-setting)
698 (defvar org-contacts--candidates-complete-doc-cache nil
699 "A list of contact candidates completion doc cache.")
701 (defun org-contacts--candidates-complete-doc-cache-setting ()
702 "Generate cache for contact candidates completion doc."
703 (if (null org-contacts--candidates-complete-doc-cache
)
704 (let* ((candidates (org-contacts--candidates-cache-setting))
705 (candidates-complete-doc-list
708 (org-contacts--candidates-org-complete-get-doc candidate
))
710 (setq org-contacts--candidates-complete-doc-cache candidates-complete-doc-list
))
711 org-contacts--candidates-complete-doc-cache
))
714 ;; (let* ((candidate (car (org-contacts--candidates-complete-doc-cache-setting)))
715 ;; (name (car candidate)))
717 ;; ;; (type-of candidate)
719 ;; (princ (text-properties-at 0 (cdr candidate)))
720 ;; (get-text-property 0 'contact-name (cdr candidate))
721 ;; (get-text-property 0 'annotation (cdr candidate)))
723 ;; (cdr (assoc "stardiviner" (org-contacts--candidates-complete-doc-cache-setting)))
725 (defun org-contacts-org-complete--doc-function (candidate)
726 "Populates *org-contact* with the documentation for the content of contact CANDIDATE."
727 (let* ((name (substring-no-properties candidate
1 nil
))
729 (lambda (contact) (string-equal (plist-get contact
:name
) name
))
730 (org-contacts-all-contacts)))
731 (contact-content (cdr (assoc name
(org-contacts--candidates-complete-doc-cache-setting))))
732 (doc-buffer (get-buffer-create " *org-contact*")))
733 (with-current-buffer doc-buffer
735 (let ((inhibit-read-only t
))
737 (insert contact-content
)
744 ;; (org-contacts-org-complete--doc-function "@stardiviner")
745 ;; (org-contacts-org-complete--doc-function (car org-contacts--candidates-cache-list))
746 ;; (benchmark 1 '(alist-get (car org-contacts--candidates-cache-list) (org-contacts--candidates-complete-doc-cache-setting)))
747 ;; (benchmark 1 '(org-contacts-org-complete--doc-function (nth 10 org-contacts--candidates-cache-list)))
749 ;; display company-mode doc buffer bellow current window.
750 (add-to-list 'display-buffer-alist
'("^ \\*org-contact\\*" .
(display-buffer-below-selected)))
752 (defun org-contacts-org-complete--exit-function (candidate)
753 (message "org-contacts: %s" (get-text-property 0 'contact-name candidate
)))
755 (defun org-contacts-org-complete--location-function (candidate)
756 "Return `org-contacts' location of contact CANDIDATE."
757 (let* ((candidate (substring-no-properties candidate
1 nil
))
759 (lambda (contact) (string-equal (plist-get contact
:name
) candidate
))
760 (org-contacts-all-contacts)))
761 (name (plist-get contact
:name
))
762 (file (plist-get contact
:file
))
763 (position (plist-get contact
:position
)))
765 (with-current-buffer (find-file-noselect file
)
767 (cons (current-buffer) position
))))
770 (defun org-contacts-org-complete-function ()
771 "`completion-at-point' function to complete @name in `org-mode'.
772 Usage: (add-hook \\='completion-at-point-functions
773 #\\='org-contacts-org-complete-function nil \\='local)"
774 (when-let* ((end (point))
775 (begin (save-excursion (skip-chars-backward "[:alnum:]@") (point)))
776 (symbol (buffer-substring-no-properties begin end
))
777 (org-contacts-prefix-p (string-prefix-p "@" symbol
)))
778 (when org-contacts-prefix-p
781 (completion-table-dynamic
784 (lambda (contact) (concat "@" (plist-get contact
:name
)))
785 (org-contacts-all-contacts))))
788 ;; properties check out `completion-extra-properties'
789 :annotation-function
#'org-contacts-org-complete--annotation-function
790 ;; TODO: change completion candidate inserted contact name into org-contact link
791 :exit-function
#'org-contacts-org-complete--exit-function
792 :company-docsig
#'identity
; metadata
793 :company-doc-buffer
#'org-contacts-org-complete--doc-function
; doc popup
794 :company-location
#'org-contacts-org-complete--location-function
))))
797 (defun org-contacts-org-complete-setup ()
798 "Setup `completion-at-point-functions' with `org-contacts' in buffer local."
799 (when (member major-mode org-contacts-completion-enabled-mode-list
)
800 (add-hook 'completion-at-point-functions
'org-contacts-org-complete-function nil
'local
)))
802 (add-hook 'org-mode-hook
#'org-contacts-org-complete-setup
)
804 (defun org-contacts-gnus-get-name-email ()
805 "Get name and email address from Gnus message."
807 (gnus-with-article-headers
808 (mail-extract-address-components
809 (or (mail-fetch-field "From") "")))))
811 (defun org-contacts-gnus-article-from-get-marker ()
812 "Return a marker for a contact based on From."
813 (let* ((address (org-contacts-gnus-get-name-email))
815 (email (cadr address
)))
816 (cl-cadar (or (org-contacts-filter
819 (cons org-contacts-email-property
(concat "\\b" (regexp-quote email
) "\\b")))
822 (concat "^" name
"$")))))))
824 (defun org-contacts-gnus-article-from-goto ()
825 "Go to contact in the From address of current Gnus message."
827 (let ((marker (org-contacts-gnus-article-from-get-marker)))
829 (switch-to-buffer-other-window (marker-buffer marker
))
831 (when (eq major-mode
'org-mode
)
832 (if (fboundp 'org-fold-show-context
)
833 (org-fold-show-context 'agenda
)
834 (org-fold-show-context 'agenda
))))))
836 (with-no-warnings (defvar date
)) ;; unprefixed, from calendar.el
839 (defun org-contacts-anniversaries (&optional field format
)
840 "Compute FIELD anniversary for each contact, returning FORMAT.
841 Default FIELD value is \"BIRTHDAY\".
843 Format is a string matching the following format specification:
846 %l - Link to the heading
848 %Y - Number of year (ordinal)"
849 (let ((calendar-date-style 'american
))
850 (unless format
(setq format org-contacts-birthday-format
))
851 (cl-loop for contact in
(org-contacts-filter)
852 for anniv
= (let ((anniv (cdr (assoc-string
853 (or field org-contacts-birthday-property
)
856 (calendar-gregorian-from-absolute
857 (org-time-string-to-absolute anniv
))))
858 ;; Use `diary-anniversary' to compute anniversary.
859 ;; FIXME: should we require `diary-lib' somewhere to be sure
860 ;; `diary-anniversary' is defined when we get here?
861 if
(and anniv
(apply #'diary-anniversary anniv
))
862 collect
(format-spec format
863 `((?l .
,(org-with-point-at (cadr contact
) (org-store-link nil
)))
864 (?h .
,(car contact
))
865 (?y .
,(- (calendar-extract-year date
)
866 (calendar-extract-year anniv
)))
867 (?Y .
,(let ((years (- (calendar-extract-year date
)
868 (calendar-extract-year anniv
))))
869 (format "%d%s" years
(diary-ordinal-suffix years
)))))))))
871 (defun org-contacts--completing-read-date ( prompt _collection
872 &optional _predicate _require-match _initial-input
873 _hist def _inherit-input-method
)
874 "Like `completing-read' but reads a date.
875 Only PROMPT and DEF are really used."
876 (org-read-date nil nil nil prompt nil def
))
878 (add-to-list 'org-property-set-functions-alist
879 `(,org-contacts-birthday-property . org-contacts--completing-read-date
))
881 (defun org-contacts-template-name (&optional return-value
)
882 "Try to return the contact name for a template.
883 If not found return RETURN-VALUE or something that would ask the user."
884 (or (car (org-contacts-gnus-get-name-email))
888 (defun org-contacts-template-email (&optional return-value
)
889 "Try to return the contact email for a template.
890 If not found return RETURN-VALUE or something that would ask the user."
891 (or (cadr (org-contacts-gnus-get-name-email))
893 (concat "%^{" org-contacts-email-property
"}p")))
895 (defun org-contacts-gnus-store-last-mail ()
896 "Store a link between mails and contacts.
898 This function should be called from `gnus-article-prepare-hook'."
899 (let ((marker (org-contacts-gnus-article-from-get-marker)))
901 (with-current-buffer (marker-buffer marker
)
904 (let* ((org-link-email-description-format (or org-contacts-email-link-description-format
905 org-link-email-description-format
))
906 (link (gnus-with-article-buffer (org-store-link nil
))))
907 (org-set-property org-contacts-last-read-mail-property link
)))))))
909 (defun org-contacts-icon-as-string ()
910 "Return the contact icon as a string."
911 (let ((image (org-contacts-get-avatar-icon)))
913 (propertize "-" 'display
917 `'(space :width
(,org-contacts-icon-size
)))
921 ;;====================================== org-contacts searching =====================================
923 (defcustom org-contacts-identity-properties-list
924 `(,org-contacts-email-property
925 ,org-contacts-alias-property
926 ,org-contacts-tel-property
927 ,org-contacts-address-property
928 ,org-contacts-birthday-property
)
929 "Matching rule for finding heading that are contacts.
930 This can be property key checking."
931 :type
'(repeat symbol
)
934 (defvar org-contacts-ahead-space-padding
(make-string 5 ?
)
935 "The space padding for align avatar image with contact name and properties.")
937 (defun org-contacts--candidate (headline)
938 "Return candidate string from Org HEADLINE epom element node."
939 (let* ((org-contacts-icon-size 32)
940 (contact-name (org-element-property :raw-value headline
))
941 (tags (org-element-property :tags headline
))
942 (properties (org-entry-properties headline
'standard
))
943 ;; extra headline properties
945 (when-let* ((avatar-value (org-entry-get headline
"AVATAR"))
946 (avatar-link-path (cond
947 ;; bracket link: [[file:contact dir/avatar image.png]]
949 ;; (when (string-match org-link-bracket-re "[[file:contact_dir/avatar image.png]]")
950 ;; (match-string 1 "[[file:contact_dir/avatar image.png]]"))
951 ;; (when (string-match org-link-bracket-re "[[attachment:avatar image.png]]")
952 ;; (match-string 1 "[[attachment:avatar image.png]]"))
953 ;; (when (string-match org-link-bracket-re "[[file:contact_dir/avatar image.png]]")
954 ;; (when-let* ((link-internal (match-string 1 "[[file:contact_dir/avatar image.png]]"))
955 ;; (_ (string-match "\\([file\\|attachment]\\):\\(.*\\)" link-internal)))
956 ;; (match-string 2 link-internal)))
957 ((or (string-match org-link-bracket-re avatar-value
)
958 (string-match org-link-any-re avatar-value
))
959 (when-let* ((link-internal (or (match-string 1 avatar-value
) (match-string 2 avatar-value
)))
960 (_ (and (or (string-prefix-p "file:" link-internal
)
961 (string-prefix-p "attachment:" link-internal
))
963 (lambda (image-extension) (string-suffix-p image-extension link-internal
))
964 image-file-name-extensions
))))
965 (when (string-match "\\([file\\|attachment]\\):\\(.*\\)" link-internal
)
966 (match-string 2 link-internal
))))
967 ;; plain link: file:/path/to/image.jpg
969 ;; (when (string-match org-link-plain-re "file:/path/to/image.jpg")
970 ;; (match-string 1 "file:/path/to/image.jpg")
971 ;; (match-string 2 "file:/path/to/image.jpg"))
972 ((string-match org-link-plain-re avatar-value
)
973 (match-string 2 avatar-value
))
974 ;; just file-name: contact-name.jpg
975 ((string-match (concat (regexp-opt image-file-name-extensions
) (rx line-end
)) avatar-value
)
976 (match-string 0 avatar-value
))))
977 (avatar-absolute-path (file-name-concat
978 (or org-contacts-directory
979 (expand-file-name (file-name-directory (car org-contacts-files
))))
981 (_ (org-file-image-p avatar-absolute-path
))
982 (_ (file-exists-p avatar-absolute-path
)))
983 avatar-absolute-path
))
985 (concat org-contacts-ahead-space-padding
" ")
986 (let ((org-property-separators (list (cons org-contacts-nickname-property
"[,\ ]"))))
987 (org-entry-get headline org-contacts-nickname-property
))
988 (let ((org-property-separators (list (cons org-contacts-email-property
"[,\ ]"))))
989 (org-entry-get headline org-contacts-email-property
))
991 (middle-line-length (let ((length (- (abs org-tags-column
)
992 (length (string-join tags
":"))
993 (length contact-name
))))
994 (if (> length
0) length
0))))
995 ;; detect whether headline is an org-contacts entry?
996 (when (seq-intersection org-contacts-identity-properties-list
(mapcar 'car properties
))
999 (if avatar-image-path
1000 (propertize org-contacts-ahead-space-padding
1001 'display
(create-image avatar-image-path nil nil
1002 :ascent
30 ; set image baseline to align image top with candidate line.
1003 :width org-contacts-icon-size
))
1004 org-contacts-ahead-space-padding
)
1008 (make-string (or middle-line-length
0) ?―
)
1009 (string-join tags
":")))
1010 'contact-name contact-name
1011 'annotation info
))))
1013 (defun org-contacts--candidates (files)
1014 "Return a list of candidates from FILES."
1016 (dolist (file files
)
1017 (insert-file-contents file
) ; don't need to actually open file.
1018 (goto-char (point-max))
1020 (delay-mode-hooks ; This will prevent user hooks from running during parsing.
1022 (goto-char (point-min))
1023 (let ((candidates nil
))
1024 (org-element-map (org-element-parse-buffer 'headline
) 'headline
1026 (when-let* ((candidate (org-contacts--candidate headline
)))
1027 (push candidate candidates
))))
1028 (nreverse candidates
)))))
1030 (defun org-contacts--annotator (candidate)
1031 "Annotate contact completion CANDIDATE."
1032 (concat (propertize " " 'display
'(space :align-to center
))
1033 (get-text-property 0 'annotation candidate
)))
1035 (defun org-contacts--candidates-return (&optional files
)
1036 "Return `org-contacts' candidates which parsed from FILES."
1037 (if-let* ((files (or files org-contacts-files
)))
1038 (org-contacts--candidates files
)
1039 (user-error "Files does not exist: %S" files
)))
1041 (defvar org-contacts--candidates-cache-list nil
1042 "A cache variable of `org-contacts--candidates' list.")
1044 (defun org-contacts--candidates-cache-reset ()
1045 "Reset `org-contacts--candidates-cache-list'."
1047 (setq org-contacts-all-contacts nil
)
1048 (setq org-contacts--candidates-cache-list nil
))
1050 (defun org-contacts--candidates-cache-setting (&optional files
)
1051 "Cache the internal variable `org-contacts--candidates-cache-list' of org-contacts candidates."
1052 (if (null org-contacts--candidates-cache-list
)
1053 (setq org-contacts--candidates-cache-list
(org-contacts--candidates-return (or files org-contacts-files
)))
1054 org-contacts--candidates-cache-list
))
1056 (defun org-contacts-browse-function (contact-name)
1057 "Jump to CONTACT-NAME headline."
1060 (let ((buf (find-file-noselect (expand-file-name file
))))
1061 (with-current-buffer buf
1062 ;; NOTE: `org-goto-marker-or-bmk' will display buffer in current window, not follow `display-buffer' rule.
1063 (when-let* ((found-contact (org-find-exact-headline-in-buffer contact-name
)))
1064 (org-goto-marker-or-bmk found-contact
)
1065 ;; FIXME: `goto-char' not physically move point in buffer.
1066 ;; (display-buffer buf '(display-buffer-below-selected))
1067 ;; (goto-char (org-find-exact-headline-in-buffer contact-name nil t))
1068 (org-fold-show-context)))))
1069 org-contacts-files
))
1072 (defun org-contacts (&optional files
)
1073 "Search `org-contacts' from FILES and jump to contact location."
1075 (org-contacts--candidates-cache-setting files
)
1076 (if-let* ((files (or files org-contacts-files
))
1077 ((seq-every-p 'file-exists-p files
)))
1078 (when-let* ((candidates org-contacts--candidates-cache-list
)
1079 (minibuffer-allow-text-properties t
)
1080 (completion-extra-properties
1081 (list :category
'org-contacts
1082 :annotation-function
#'org-contacts--annotator
))
1083 (choice (completing-read "org-contacts: " candidates nil
'require-match
))
1084 (contact-name (get-text-property 0 'contact-name choice
)))
1085 ;; jump to org-contacts file contact position.
1086 (org-contacts-browse-function contact-name
))
1087 (user-error "Files does not exist: %S" files
)))
1090 (defun org-contacts-agenda (name)
1091 "Create agenda view for contacts matching NAME."
1092 (interactive (list (read-string "Name: ")))
1093 (let ((org-agenda-files (org-contacts-files))
1094 (org-agenda-skip-function
1095 (lambda () (org-agenda-skip-if nil
`(notregexp ,name
))))
1096 (org-agenda-prefix-format
1098 "%(org-contacts-icon-as-string)% s%(org-contacts-irc-number-of-unread-messages) "
1099 'keymap org-contacts-keymap
))
1100 (org-agenda-overriding-header
1101 (or org-agenda-overriding-header
1102 (concat "List of contacts matching `" name
"':"))))
1103 (setq org-agenda-skip-regexp name
)
1104 (org-tags-view nil org-contacts-matcher
)
1105 (with-current-buffer org-agenda-buffer-name
1106 (setq org-agenda-redo-command
1107 (list 'org-contacts name
)))))
1109 (defun org-contacts-completing-read (prompt
1111 initial-input hist def inherit-input-method
)
1112 "Call `completing-read' with contacts name as collection."
1113 (org-completing-read
1114 prompt
(org-contacts-filter) predicate t initial-input hist def inherit-input-method
))
1116 (defun org-contacts-format-name (name)
1117 "Trim any local formatting to get a bare NAME."
1118 ;; Remove radio targets characters
1119 (replace-regexp-in-string org-radio-target-regexp
"\\1" name
))
1121 (defun org-contacts-format-email (name email
)
1122 "Format an EMAIL address corresponding to NAME."
1124 (error "`email' cannot be nul"))
1126 (concat (org-contacts-format-name name
) " <" email
">")
1129 (defun org-contacts-check-mail-address (mail)
1130 "Add MAIL address to contact at point if it does not have it."
1131 (let ((mails (org-entry-get (point) org-contacts-email-property
)))
1132 (unless (member mail
(split-string mails
))
1134 (format "Do you want to add this address to %s?" (org-get-heading t
)))
1135 (org-set-property org-contacts-email-property
(concat mails
" " mail
))))))
1137 (defun org-contacts-gnus-check-mail-address ()
1138 "Check that contact has the current address recorded.
1139 This function should be called from `gnus-article-prepare-hook'."
1140 (let ((marker (org-contacts-gnus-article-from-get-marker)))
1142 (org-with-point-at marker
1143 (org-contacts-check-mail-address (cadr (org-contacts-gnus-get-name-email)))))))
1145 (defun org-contacts-gnus-insinuate ()
1146 "Add some hooks for Gnus user.
1147 This adds `org-contacts-gnus-check-mail-address' and
1148 `org-contacts-gnus-store-last-mail' to
1149 `gnus-article-prepare-hook'. It also adds a binding on `;' in
1150 `gnus-summary-mode-map' to `org-contacts-gnus-article-from-goto'"
1153 (define-key gnus-summary-mode-map
";" #'org-contacts-gnus-article-from-goto
)
1154 (add-hook 'gnus-article-prepare-hook
#'org-contacts-gnus-check-mail-address
)
1155 (add-hook 'gnus-article-prepare-hook
#'org-contacts-gnus-store-last-mail
))
1158 (defun org-contacts-email-setup-completion-at-point ()
1159 "Add `org-contacts-message-complete-function' to capf for completing contact at point."
1160 (when (member major-mode org-contacts-completion-enabled-mode-list
)
1161 (add-hook 'completion-at-point-functions
'org-contacts-message-complete-function nil
'local
)))
1163 (add-hook 'message-mode-hook
#'org-contacts-email-setup-completion-at-point
)
1164 (when (featurep 'mu4e
)
1165 (add-hook 'mu4e-compose-mode-hook
'org-contacts-email-setup-completion-at-point
))
1167 (defun org-contacts-wl-get-from-header-content ()
1168 "Retrieve the content of the `From' header of an email.
1169 Works from wl-summary-mode and mime-view-mode - that is while viewing email.
1170 Depends on Wanderlust been loaded."
1171 (with-current-buffer (org-capture-get :original-buffer
)
1173 ((eq major-mode
'wl-summary-mode
) (when (and (boundp 'wl-summary-buffer-elmo-folder
)
1174 wl-summary-buffer-elmo-folder
)
1176 wl-summary-buffer-elmo-folder
1177 (wl-summary-message-number)
1179 ((eq major-mode
'mime-view-mode
) (std11-narrow-to-header)
1181 (std11-fetch-field "From")
1184 (defun org-contacts-wl-get-name-email ()
1185 "Get name and email address from Wanderlust email.
1186 See `org-contacts-wl-get-from-header-content' for limitations."
1187 (let ((from (org-contacts-wl-get-from-header-content)))
1189 (list (wl-address-header-extract-realname from
)
1190 (wl-address-header-extract-address from
)))))
1192 (defun org-contacts-template-wl-name (&optional return-value
)
1193 "Try to return the contact name for a template from wl.
1194 If not found, return RETURN-VALUE or something that would ask the
1196 (or (car (org-contacts-wl-get-name-email))
1200 (defun org-contacts-template-wl-email (&optional return-value
)
1201 "Try to return the contact email for a template from Wanderlust.
1202 If not found return RETURN-VALUE or something that would ask the user."
1203 (or (cadr (org-contacts-wl-get-name-email))
1205 (concat "%^{" org-contacts-email-property
"}p")))
1207 (defun org-contacts-view-send-email (&optional ask
)
1208 "Send email to the contact at point.
1209 If ASK is set, ask for the email address even if there's only one
1212 (let ((marker (org-get-at-bol 'org-hd-marker
)))
1213 (org-with-point-at marker
1214 (let ((emails (org-entry-get (point) org-contacts-email-property
)))
1216 (let ((email-list (org-contacts-split-property emails
)))
1217 (if (and (= (length email-list
) 1) (not ask
))
1218 (compose-mail (org-contacts-format-email
1219 (org-get-heading t
) emails
))
1220 (let ((email (completing-read "Send mail to which address: " email-list
)))
1221 (setq email
(org-contacts-strip-link email
))
1222 (org-contacts-check-mail-address email
)
1223 (compose-mail (org-contacts-format-email (org-get-heading t
) email
)))))
1224 (error (format "This contact has no mail address set (no %s property)"
1225 org-contacts-email-property
)))))))
1227 (defun org-contacts-get-avatar-icon (&optional pom
)
1228 "Get icon for contact at POM and return the avatar icon image object."
1229 (setq pom
(or pom
(point)))
1231 ;; Use `org-contacts-icon-property'
1232 (let* ((link-matcher-regexp
1233 "\\[\\[\\([^]]*\\)\\]\\(\\[\\(.*\\)\\]\\)?\\]")
1234 (contacts-dir (file-name-directory (car (org-contacts-files))))
1236 (if-let* ((avatar (org-entry-get pom org-contacts-icon-property
)))
1238 ;; [[file:dir/filename.png]]
1239 ((string-match-p "\\[\\[.*\\]\\]" avatar
)
1240 ;; FIXME: What if avatar matches the above regexp but the one below?
1241 (when (string-match link-matcher-regexp avatar
)
1242 ;; FIXME: 5 seems to be the length of `file:' but I can't see anything that
1243 ;; guarantees that the submatch 1 starts with `file:'.
1244 (expand-file-name (substring (match-string-no-properties 1 avatar
) 5 nil
)
1246 ;; "" (empty string)
1247 ((string-empty-p avatar
) nil
)
1248 (t (expand-file-name avatar contacts-dir
))))))
1251 (if (featurep 'imagemagick
)
1252 (create-image image-path
'imagemagick nil
:ascent
100 :height org-contacts-icon-size
)
1253 (create-image image-path nil nil
:ascent
100 :height org-contacts-icon-size
)))))
1254 ;; Next, try Gravatar
1255 (when org-contacts-icon-use-gravatar
1256 (defvar gravatar-size
)
1257 (let* ((gravatar-size org-contacts-icon-size
)
1258 (email-list (org-entry-get pom org-contacts-email-property
))
1261 (cl-loop for email in
(org-contacts-split-property email-list
)
1262 for gravatar
= (gravatar-retrieve-synchronously (org-contacts-strip-link email
))
1264 (not (eq gravatar
'error
)))
1266 (when gravatar
(throw 'icon gravatar
))))))
1268 (defun org-contacts-irc-buffer (&optional pom
)
1269 "Get the IRC buffer associated with the entry at POM."
1270 (setq pom
(or pom
(point)))
1271 (let ((nick (org-entry-get pom org-contacts-nickname-property
)))
1273 (let ((buffer (get-buffer nick
)))
1275 (with-current-buffer buffer
1276 (when (eq major-mode
'erc-mode
)
1279 (defun org-contacts-irc-number-of-unread-messages (&optional pom
)
1280 "Return the number of unread messages for contact at POM."
1281 (when (boundp 'erc-modified-channels-alist
)
1282 (let ((number (cadr (assoc (org-contacts-irc-buffer pom
) erc-modified-channels-alist
))))
1284 (format (concat "%3d unread message" (if (> number
1) "s" " ") " ") number
)
1285 (make-string 21 ?
)))))
1287 (defun org-contacts-view-switch-to-irc-buffer ()
1288 "Switch to the IRC buffer of the current contact if it has one."
1290 (let ((marker (org-get-at-bol 'org-hd-marker
)))
1291 (org-with-point-at marker
1292 (switch-to-buffer-other-window (org-contacts-irc-buffer)))))
1294 (defun org-contacts-completing-read-nickname
1296 &optional predicate require-match initial-input
1297 hist def inherit-input-method
)
1298 "Like `completing-read' but reads a property \"NICKNAME\" value in PROMPT.
1299 Return a org-contacts \"NICKNAME\" as property's value after completion."
1300 (let* ((org-contacts-candidates-propertized
1303 (let* ((name (plist-get plist
:name
))
1304 (name-english (plist-get plist
:name-english
))
1305 (nick (plist-get plist
:nick
)))
1306 (unless (or (null nick
) (string-empty-p nick
))
1307 (propertize nick
; <- The `completing-read' select candidate inserted value.
1309 (when name
(propertize (format "%s " name
) :face
'(:foreground
"ForestGreen")))
1310 (unless (or (null name-english
) (string-empty-p name-english
))
1311 (propertize (format "%s " name-english
) :face
'(:foreground
"LightSeaGreen")))
1312 (unless (or (null nick
) (string-empty-p nick
))
1313 (propertize (format "(%s) " nick
) :face
'(:foreground
"LightGray"))))))))
1314 (org-contacts-all-contacts)))
1315 ;; (contact-names (mapcar (lambda (plist) (plist-get plist :name)) (org-contacts-all-contacts)))
1316 (contact-nick (substring-no-properties
1317 (org-completing-read (or prompt
"org-contacts NICKNAME: ")
1318 (append org-contacts-candidates-propertized collection
1319 (when (or (require 'erc nil t
)
1320 (erc-server-buffer-live-p)
1321 (erc-server-process-alive)
1322 erc-server-processing-p
)
1323 (org-contacts-erc-nicknames-list)))
1324 predicate require-match initial-input
1325 hist def inherit-input-method
))))
1326 ;; Detect whether input contact is in `org-contacts' existing list.
1329 (defun org-contacts-erc-nicknames-list ()
1330 "Return all nicknames of all ERC buffers."
1331 (cl-loop for buffer in
(erc-buffer-list)
1332 nconc
(with-current-buffer buffer
1333 (cl-loop for user-entry
1334 in
(mapcar #'car
(erc-get-channel-user-list))
1335 collect
(elt user-entry
1)))))
1337 (add-to-list 'org-property-set-functions-alist
1338 `(,org-contacts-nickname-property . org-contacts-completing-read-nickname
))
1340 (defun org-contacts-vcard-escape (str)
1341 "Escape ; , and \n in STR for the VCard format."
1342 ;; Thanks to this library for the regexp:
1343 ;; https://www.emacswiki.org/cgi-bin/wiki/bbdb-vcard-export.el
1345 (replace-regexp-in-string
1347 (replace-regexp-in-string "\\(;\\|,\\|\\\\\\)" "\\\\\\1" str
))))
1349 (defun org-contacts-vcard-encode-name (name)
1350 "Try to encode NAME as VCard's N property.
1351 The N property expects
1353 FamilyName;GivenName;AdditionalNames;Prefix;Postfix.
1355 Org-contacts does not specify how to encode the name. So we try
1357 (concat (replace-regexp-in-string "\\(\\w+\\) \\(.*\\)" "\\2;\\1" name
) ";;;"))
1359 (defun org-contacts-vcard-format (contact)
1360 "Formats CONTACT in VCard 3.0 format."
1361 (let* ((properties (nth 2 contact
))
1362 (name (org-contacts-vcard-escape (car contact
)))
1363 (n (org-contacts-vcard-encode-name name
))
1364 (email (cdr (assoc-string org-contacts-email-property properties
)))
1365 (tel (cdr (assoc-string org-contacts-tel-property properties
)))
1366 (ignore-list (cdr (assoc-string org-contacts-ignore-property properties
)))
1367 (ignore-list (when ignore-list
1368 (org-contacts-split-property ignore-list
)))
1369 (note (cdr (assoc-string org-contacts-note-property properties
)))
1370 (bday (org-contacts-vcard-escape (cdr (assoc-string org-contacts-birthday-property properties
))))
1371 (addr (cdr (assoc-string org-contacts-address-property properties
)))
1372 (nick (org-contacts-vcard-escape (cdr (assoc-string org-contacts-nickname-property properties
))))
1373 (categories (mapconcat (lambda (str) (concat "" str
))
1374 (delq "" (string-split (string-trim (cdr (assoc-string "TAGS" properties
)) ":" ":") ":"))
1376 (head (format "BEGIN:VCARD\nVERSION:3.0\nN:%s\nFN:%s\n" n name
))
1377 emails-list result phones-list
)
1382 (setq emails-list
(org-contacts-remove-ignored-property-values
1383 ignore-list
(org-contacts-split-property email
)))
1386 (setq result
(concat result
"EMAIL:" (org-contacts-strip-link (car emails-list
)) "\n"))
1387 (setq emails-list
(cdr emails-list
)))
1390 (format "ADR:;;%s\n" (replace-regexp-in-string "\\, ?" ";" addr
)))
1393 (setq phones-list
(org-contacts-remove-ignored-property-values
1394 ignore-list
(org-contacts-split-property tel
)))
1397 (setq result
(concat result
"TEL:" (org-contacts-strip-link
1398 (org-link-unescape (car phones-list
))) "\n"))
1399 (setq phones-list
(cdr phones-list
)))
1402 (let ((cal-bday (calendar-gregorian-from-absolute (org-time-string-to-absolute bday
))))
1403 (format "BDAY:%04d-%02d-%02d\n"
1404 (calendar-extract-year cal-bday
)
1405 (calendar-extract-month cal-bday
)
1406 (calendar-extract-day cal-bday
))))
1407 (when nick
(format "NICKNAME:%s\n" nick
))
1408 (when categories
(format "CATEGORIES:%s\n" categories
))
1409 (when note
(format "NOTE:%s\n" note
))
1412 (defun org-contacts-export-as-vcard (&optional name file to-buffer
)
1413 "Export org-contacts to V-Card 3.0.
1415 By default, all contacts are exported to `org-contacts-vcard-file'.
1417 When NAME is \\[universal-argument], prompts for a contact name.
1419 When NAME is \\[universal-argument] \\[universal-argument],
1420 prompts for a contact name and a file name where to export.
1422 When NAME is \\[universal-argument] \\[universal-argument]
1423 \\[universal-argument], prompts for a contact name and a buffer where to export.
1425 If the function is not called interactively, all parameters are
1426 passed to `org-contacts-export-as-vcard-internal'."
1428 (when (called-interactively-p 'any
)
1431 (read-string "Contact name: "
1432 (nth 0 (org-contacts-at-point))))
1434 (when (equal name
'(16))
1435 (read-file-name "File: " nil org-contacts-vcard-file
))
1437 (when (equal name
'(64))
1438 (read-buffer "Buffer: "))))
1439 (org-contacts-export-as-vcard-internal name file to-buffer
))
1441 (defun org-contacts-export-as-vcard-internal (&optional name file to-buffer
)
1442 "Export all contacts matching NAME as VCard 3.0.
1443 If TO-BUFFER is nil, the content is written to FILE or
1444 `org-contacts-vcard-file'. If TO-BUFFER is non-nil, the buffer
1445 is created and the VCard is written into that buffer."
1446 (let* ((filename (or file org-contacts-vcard-file
))
1447 (buffer (if to-buffer
1448 (get-buffer-create to-buffer
)
1449 (find-file-noselect filename
))))
1450 (message "Exporting...")
1452 (let ((inhibit-read-only t
)) (erase-buffer))
1454 (when (fboundp 'set-buffer-file-coding-system
)
1455 (set-buffer-file-coding-system coding-system-for-write
))
1456 (cl-loop for contact in
(org-contacts-filter name
)
1457 do
(insert (org-contacts-vcard-format contact
)))
1460 (progn (save-buffer) (kill-buffer)))))
1462 (defun org-contacts-show-map (&optional name
)
1463 "Show contacts on a map.
1464 Requires google-maps.el."
1466 (unless (fboundp 'google-maps-static-show
)
1467 (error "`org-contacts-show-map' requires `google-maps.el'"))
1468 (google-maps-static-show
1471 for contact in
(org-contacts-filter name
)
1472 for addr
= (cdr (assoc-string org-contacts-address-property
(nth 2 contact
)))
1474 collect
(cons (list addr
) (list :label
(string-to-char (car contact
)))))))
1476 (defun org-contacts-strip-link (link)
1477 "Strip Org LINK extra delimiters.
1479 Remove brackets, description, link type and colon from an Org link
1480 string and return the pure link target."
1481 (let (startpos colonpos endpos
)
1482 (setq startpos
(string-match (regexp-opt '("[[tel:" "[[mailto:")) link
))
1485 (setq colonpos
(string-match ":" link
))
1486 (setq endpos
(string-match "\\]" link
))
1487 (if endpos
(substring link
(1+ colonpos
) endpos
) link
))
1489 (setq startpos
(string-match "mailto:" link
))
1490 (setq colonpos
(string-match ":" link
))
1491 (if startpos
(substring link
(1+ colonpos
)) link
)))))
1493 ;; Add the link type supported by org-contacts-strip-link
1494 ;; so everything is in order for its use in Org files
1495 (if (fboundp 'org-link-set-parameters
)
1496 (org-link-set-parameters "tel")
1497 (if (fboundp 'org-add-link-type
)
1498 (org-add-link-type "tel")))
1500 (defun org-contacts-split-property (string &optional separators omit-nulls
)
1501 "Custom version of `split-string'.
1502 Split a property STRING into sub-strings bounded by matches
1503 for SEPARATORS but keep Org links intact.
1505 The beginning and end of STRING, and each match for SEPARATORS, are
1506 splitting points. The substrings matching SEPARATORS are removed, and
1507 the substrings between the splitting points are collected as a list,
1510 If SEPARATORS is non-nil, it should be a regular expression
1511 matching text which separates, but is not part of, the
1512 substrings. If nil it defaults to `org-contacts-property-values-separators',
1513 normally \"[,; \\f\\t\\n\\r\\v]+\", and OMIT-NULLS is forced to t.
1515 If OMIT-NULLS is t, zero-length substrings are omitted from the list so
1516 that for the default value of SEPARATORS leading and trailing whitespace
1517 are effectively trimmed. If nil, all zero-length substrings are retained."
1518 (let* ((omit-nulls (if separators omit-nulls t
))
1519 (rexp (or separators org-contacts-property-values-separators
))
1520 (inputlist (split-string string rexp omit-nulls
))
1523 (proplist (list "")))
1525 (setq bufferstring
(pop inputlist
))
1526 (if (string-match "\\[\\[" bufferstring
)
1528 (setq linkstring
(concat bufferstring
" "))
1529 (while (not (string-match "\\]\\]" bufferstring
))
1530 (setq bufferstring
(pop inputlist
))
1531 (setq linkstring
(concat linkstring bufferstring
" ")))
1532 (setq proplist
(cons (org-trim linkstring
) proplist
)))
1533 (setq proplist
(cons bufferstring proplist
))))
1534 (cdr (reverse proplist
))))
1537 ;;; Add an Org link type `org-contact:' for easy jump to or searching org-contacts headline.
1538 ;; link spec: [[org-contact:query][desc]]
1540 (if (fboundp 'org-link-set-parameters
)
1541 (org-link-set-parameters "org-contact"
1542 :follow
#'org-contacts-link-open
1543 :complete
#'org-contacts-link-complete
1544 :store
#'org-contacts-link-store
1545 :face
'org-contacts-link-face
)
1546 (when (fboundp 'org-add-link-type
)
1547 (org-add-link-type "org-contact" 'org-contacts-link-open
)))
1550 (defun org-contacts-link-store ()
1551 "Store the contact in `org-contacts-files' with a link."
1552 (when (and (eq major-mode
'org-mode
)
1553 (member (buffer-file-name)
1554 (mapcar #'expand-file-name
(org-contacts-files)))
1555 (not (org-before-first-heading-p))
1556 (let ((element (org-element-at-point)))
1557 (funcall (cdr (org-make-tags-matcher org-contacts-matcher
))
1558 (org-element-property :todo-keyword element
)
1559 (org-get-tags element
)
1560 (org-element-property :level element
))))
1561 (if (bound-and-true-p org-id-link-to-org-use-id
)
1563 (let ((headline-str (substring-no-properties (org-get-heading t t t t
))))
1564 (org-link-store-props
1567 :description headline-str
)
1568 (let ((link (concat "org-contact:" headline-str
)))
1569 (org-link-add-props :link link
:description headline-str
)
1572 (defun org-contacts--all-contacts ()
1573 "Return a list of all contacts in `org-contacts-files'.
1574 Each element has the form (NAME . (FILE . POSITION))."
1577 (unless (buffer-live-p (get-buffer (file-name-nondirectory file
)))
1578 (find-file-noselect file
))
1579 (with-current-buffer (find-file-noselect file
)
1582 (let* ((name (substring-no-properties (org-get-heading t t t t
)))
1583 (file (buffer-file-name))
1585 ;; extract properties Org entry headline at `position' as data API for better contacts searching.
1586 (entry-properties (org-entry-properties position
'standard
))
1587 (property-name-chinese (cdr (assoc (upcase "NAME(Chinese)") entry-properties
)))
1588 (property-name-english (cdr (assoc (upcase "NAME(English)") entry-properties
)))
1589 (property-nick (cdr (assoc "NICK" entry-properties
)))
1590 (property-email (cdr (assoc "EMAIL" entry-properties
)))
1591 ;; (property-mobile (cdr (assoc "MOBILE" entry-properties)))
1592 (property-wechat (cdr (assoc (upcase "WeChat") entry-properties
)))
1593 (property-qq (cdr (assoc "QQ" entry-properties
))))
1594 (list :name name
:file file
:position position
1595 :name-chinese property-name-chinese
1596 :name-english property-name-english
1598 :email property-email
1599 :mobile property-email
1600 :wechat property-wechat
1601 :qq property-qq
))))))
1602 (org-contacts-files))))
1605 (defun org-contacts-link-open (query)
1606 "Open org-contacts: link with jumping or searching QUERY."
1607 (let* ((file-path (car (org-contacts-files)))
1608 (file-name (file-name-nondirectory file-path
))
1609 (buf (or (get-buffer file-name
) (get-buffer (find-file-noselect file-path
)))))
1611 ;; /query/ format searching
1612 ((string-match "/.*/" query
)
1613 (with-current-buffer buf
1614 (string-match "/\\(.*\\)/" query
)
1615 (occur (match-string 1 query
))))
1617 ;; jump to exact contact headline directly
1619 (with-current-buffer buf
1620 (if-let* ((position (org-find-exact-headline-in-buffer query
)))
1622 (goto-char (marker-position position
))
1623 (org-fold-show-context))
1624 (user-error "[org-contacts] Can't find <%s> in your `org-contacts-files'" query
)))
1625 (display-buffer buf
'(display-buffer-below-selected))
1628 ;; (let* ((contact-entry (map-filter
1629 ;; (lambda (contact-plist)
1630 ;; (if (string-equal (plist-get contact-plist :name) query)
1632 ;; (org-contacts-all-contacts)))
1633 ;; (contact-name (plist-get contact-entry :name))
1634 ;; (file (plist-get contact-entry :file))
1635 ;; (position (plist-get contact-entry :position))
1636 ;; (buf (get-buffer (file-name-nondirectory file))))
1637 ;; (with-current-buffer buf (goto-char position))
1638 ;; (display-buffer buf '(display-buffer-below-selected)))
1642 (defun org-contacts-link-complete (&optional _arg
)
1643 "Create a `org-contacts' link using completion."
1644 (let ((name (completing-read "org-contacts NAME: "
1646 (lambda (plist) (plist-get plist
:name
))
1647 (org-contacts-all-contacts)))))
1648 (concat "org-contact:" name
)))
1650 (defun org-contacts-link-face (path)
1651 "Different face color for different org-contacts: link PATH."
1653 ((string-match "/.*/" path
)
1654 '(:background
"sky blue" :overline t
:slant
'italic
))
1655 (t '(:inherit org-link
))))
1658 ;;; org-mode link "mailto:" email completion.
1659 (if (fboundp 'org-link-set-parameters
)
1660 (org-link-set-parameters "mailto" :complete
#'org-contacts-mailto-link-completion
)
1661 (if (fboundp 'org-add-link-type
)
1662 (org-add-link-type "mailto")))
1664 (defvar org-contacts-emails-list nil
1665 "A list variable of all `org-contacts' emails.")
1667 (defun org-contacts-mailto-link--get-all-emails ()
1668 "Retrieve all `org-contacts' EMAIL property values."
1669 (setq org-contacts-emails-list
1672 (let* ((org-contacts-buffer (find-file-noselect (car (org-contacts-files))))
1673 (name (plist-get contact
:name
))
1674 (position (plist-get contact
:position
))
1675 (email (save-excursion
1676 (with-current-buffer org-contacts-buffer
1677 (goto-char position
)
1678 ;; (symbol-name (org-property-or-variable-value 'EMAIL))
1679 (when-let* ((pvalue (org-entry-get (point) "EMAIL")))
1680 ;; handle `mailto:' link. e.g. "[[mailto:yantar92@posteo.net]]", "[[mailto:yantar92@posteo.net][yantar92@posteo.net]]"
1681 ;; Reference the testing file `test-org-contacts.el'.
1683 "\\[\\[mailto:\\(.*\\)\\]\\(\\[.*\\]\\)\\]\\(,\\ *\\[\\[mailto:\\(.*\\)\\]\\(\\[.*\\]\\)\\]\\)"
1685 (match-string 1 pvalue
)
1688 ;; (cons name email)
1690 (org-contacts-all-contacts)))
1691 ;; clean nil and empty string "" from result.
1693 (delete nil org-contacts-emails-list
)))
1695 (defun org-contacts-mailto-link-completion (&optional _arg
)
1696 "Org mode link `mailto:' completion with `org-contacts' emails."
1697 (let ((email (completing-read "org-contacts EMAIL: "
1698 (org-contacts-mailto-link--get-all-emails))))
1699 (concat "mailto:" email
)))
1701 (provide 'org-contacts
)
1703 ;;; org-contacts.el ends here