emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/org-contacts 4e55b091c9: Implement a new org-contacts s


From: ELPA Syncer
Subject: [elpa] externals/org-contacts 4e55b091c9: Implement a new org-contacts searching & completing through all contacts
Date: Tue, 9 Apr 2024 09:58:29 -0400 (EDT)

branch: externals/org-contacts
commit 4e55b091c9d164bfdae1c5b93a3865e0499728b1
Author: stardiviner <numbchild@gmail.com>
Commit: stardiviner <numbchild@gmail.com>

    Implement a new org-contacts searching & completing through all contacts
---
 README.org      |  14 +-----
 org-contacts.el | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 142 insertions(+), 13 deletions(-)

diff --git a/README.org b/README.org
index 70ed56fb39..41e1b2a7e2 100644
--- a/README.org
+++ b/README.org
@@ -26,16 +26,6 @@ Package has been submitted to NonGNU or MELPA. You can 
install it through those
 
 ** Search contact in org-contacts databse
 
-Use command =[M-x org-contacts]= to search.
-
-** Complete contact property with some functions support
-
-*** NAME
-
-*** NICK
-
-*** EMAIL
-
-*** BIRTHDAY
-
+- Use command =[M-x org-contacts]= to search and select concate through 
~completing-read~.
+- Use command =[M-x org-contacts-agenda]= for old behavior Org Agenda matching.
 
diff --git a/org-contacts.el b/org-contacts.el
index dd09d077e0..6acfd39e38 100644
--- a/org-contacts.el
+++ b/org-contacts.el
@@ -82,6 +82,11 @@
   "Options about contacts management."
   :group 'org)
 
+(defcustom org-contacts-directory nil
+  "Directory of Org files to use as contacts source.
+When set to nil, all your Org files will be used."
+  :type 'string)
+
 (defcustom org-contacts-files nil
   "List of Org files to use as contacts source.
 When set to nil, all your Org files will be used."
@@ -836,8 +841,142 @@ This function should be called from 
`gnus-article-prepare-hook'."
                   '(:ascent center)))
      " ")))
 
+;;====================================== org-contacts searching 
=====================================
+
+(defcustom org-contacts-identity-properties-list
+  (list org-contacts-email-property
+        org-contacts-alias-property
+        org-contacts-tel-property
+        org-contacts-address-property
+        org-contacts-birthday-property)
+  "Matching rule for finding heading that are contacts.
+This can be property key checking."
+  :type 'list
+  :safe 'listp)
+
+(defvar org-contacts-ahead-space-padding (make-string 5 ? )
+  "The space padding for align avatar image with contact name and properties.")
+
+(defun org-contacts--candidate (headline)
+  "Return candidate string from Org HEADLINE epom element node."
+  (let* ((org-contacts-icon-size 32)
+         (contact-name (org-element-property :raw-value headline))
+         (tags (org-element-property :tags headline))
+         (properties (org-entry-properties headline 'standard))
+         ;; extra headline properties
+         (avatar-image-path
+          (when-let* ((avatar-value (car (org-entry-get-multivalued-property 
headline "AVATAR")))
+                      (avatar-link-path (cond
+                                         ;; [[file:contact_dir/avatar.png]]
+                                         ((string-match org-link-plain-re 
avatar-value)
+                                          (when (string-equal (match-string 1 
avatar-value) "file")
+                                            (match-string 2 avatar-value)))
+                                         ;; contact-name.jpg
+                                         ((string-match (concat (regexp-opt 
image-file-name-extensions) (rx line-end)) avatar-value)
+                                          (match-string 0 avatar-value))))
+                      (avatar-absolute-path (file-name-concat
+                                             (or org-contacts-directory
+                                                 (expand-file-name 
(file-name-directory (car org-contacts-files))))
+                                             avatar-link-path))
+                      ( (org-file-image-p avatar-absolute-path))
+                      ( (file-exists-p avatar-absolute-path)))
+            avatar-absolute-path))
+         (info (concat "\n"
+                       (concat org-contacts-ahead-space-padding "   ")
+                       (string-join (let ((org-property-separators (list (cons 
org-contacts-nickname-property "[,\ ]"))))
+                                      (org-entry-get-multivalued-property 
headline org-contacts-nickname-property)) ", ")
+                       (string-join (let ((org-property-separators (list (cons 
org-contacts-email-property "[,\ ]"))))
+                                      (org-entry-get-multivalued-property 
headline org-contacts-email-property)) ", ")
+                       "\n"))
+         (middle-line-length (when-let* ((length (- (- org-tags-column)
+                                                    (length (string-join tags 
":"))
+                                                    (length contact-name)))
+                                         (wholenump length))
+                               length)))
+    ;; detect whether headline is an org-contacts entry?
+    (when (seq-intersection (mapcar 'car properties) 
org-contacts-identity-properties-list)
+      (propertize
+       (concat
+        (if avatar-image-path
+            (propertize org-contacts-ahead-space-padding
+                        'display (create-image avatar-image-path nil nil
+                                               :ascent 30 ; set image baseline 
to align image top with candidate line.
+                                               :width org-contacts-icon-size))
+          org-contacts-ahead-space-padding)
+        " "
+        contact-name
+        (format " %s [%s]"
+                (make-string (or middle-line-length 0) ?―)
+                (string-join tags ":")))
+       'contact-name contact-name
+       'annotation info))))
+
+(defun org-contacts--candidates (files)
+  "Return a list of candidates from FILES."
+  (with-temp-buffer
+    (dolist (file files)
+      (insert-file-contents file) ; don't need to actually open file.
+      (goto-char (point-max))
+      (newline 2))
+    (delay-mode-hooks ; This will prevent user hooks from running during 
parsing.
+      (org-mode)
+      (goto-char (point-min))
+      (let ((candidates nil))
+        (org-element-map (org-element-parse-buffer 'headline) 'headline
+          (lambda (headline)
+            (when-let ((candidate (org-contacts--candidate headline)))
+              (push candidate candidates))))
+        (nreverse candidates)))))
+
+(defun org-contacts--annotator (candidate)
+  "Annotate contact completion CANDIDATE."
+  (concat (propertize " " 'display '(space :align-to center))
+          (get-text-property 0 'annotation candidate)))
+
+(defun org-contacts--return-candidates (&optional files)
+  "Return org-contacts candidates which parsed from FILES."
+  (if-let ((files (or files org-contacts-files)))
+      (org-contacts--candidates files)
+    (user-error "Files does not exist: %S" files)))
+
+(defvar org-contacts--candidates-cache nil
+  "A cache variable of org-contacts--candidates.")
+
+(defun org-contacts-browse-function (contact-name)
+  "Jump to CONTACT-NAME headline."
+  (mapcar
+   (lambda (file)
+     (let ((buf (find-file-noselect (expand-file-name file))))
+       (with-current-buffer buf
+         ;; NOTE: `org-goto-marker-or-bmk' will display buffer in current 
window, not follow `display-buffer' rule.
+         (org-goto-marker-or-bmk (org-find-exact-headline-in-buffer 
contact-name))
+         ;; FIXME: `goto-char' not physically move point in buffer.
+         ;; (display-buffer buf '(display-buffer-below-selected))
+         ;; (goto-char (org-find-exact-headline-in-buffer contact-name nil t))
+         )))
+   org-contacts-files))
+
+;;;###autoload
+(defun org-contacts (&optional files)
+  "Search org-contacts from FILES and jump to contact location."
+  (interactive)
+  (unless org-contacts--candidates-cache
+    (setq org-contacts--candidates-cache (org-contacts--return-candidates 
files)))
+  (if-let* ((files (or files org-contacts-files))
+            ((seq-every-p 'file-exists-p files)))
+      (when-let* ((candidates org-contacts--candidates-cache)
+                  (minibuffer-allow-text-properties t)
+                  (completion-extra-properties
+                   (list :category 'org-contacts
+                         :annotation-function #'org-contacts--annotator))
+                  (choice (completing-read "org-contacts: " candidates nil 
'require-match))
+                  (contact-name (get-text-property 0 'contact-name choice)))
+        ;; jump to org-contacts file contact position.
+        (org-contacts-browse-function contact-name))
+    (user-error "Files does not exist: %S" files)))
+
 ;;;###autoload
-(defun org-contacts (name)
+(defun org-contacts-agenda (name)
   "Create agenda view for contacts matching NAME."
   (interactive (list (read-string "Name: ")))
   (let ((org-agenda-files (org-contacts-files))



reply via email to

[Prev in Thread] Current Thread [Next in Thread]