123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- ;;; khard.el --- Emacs integration with khard -*- lexical-binding: t -*-
- ;;; Commentary:
- ;;; Code:
- (require 'with-editor)
- (require 'cl-lib)
- (require 'message)
- (defcustom khard-executable "khard"
- "The executable to use to run khard."
- :group 'khard
- :type 'string)
- (defvar-local khard--contacts-cache ()
- "List of contacts used while completing at point.
- This exists so that Emacs doesn't slow down while running
- `completion-at-point-functions'. This is local to each buffer.")
- (defun khard--build-list-entry-detail (&rest items)
- "Build a detail in the format \" (ITEMS)\", or an empty string."
- (let ((clean-items (remove "" items)))
- (if clean-items
- (format " (%s)" (string-join clean-items ", "))
- "")))
- (defun khard--build-uid-email-phone-list ()
- "Build a list in the format (info . uid)."
- (cl-loop for line in
- (process-lines "khard" "ls"
- "--parsable" "--fields=uid,name,email,phone")
- for (uid name email phone) = (split-string line "\t")
- collect
- (cons (format "%s%s" name
- (khard--build-list-entry-detail email phone uid))
- uid)))
- (defun khard--prompt-contact (&optional prompt)
- "Prompt user for a contact, optionally make the prompt text PROMPT."
- (if-let ((uid-list (khard--build-uid-email-phone-list))
- (resp (completing-read (or prompt "Contact ") uid-list)))
- (assoc resp uid-list)))
- (defun khard--process-sentinel (proc status)
- "Process sentinel for kahrd commands.
- For info on PROC and STATUS, see `set-process-sentinel'."
- (when (memq (process-status proc) '(exit signal))
- (message "khard: %s." (substring status 0 -1))))
- (cl-defun khard--run-khard (args &key filter)
- "Run khard with ARGS.
- FILTER is a process filter to install on the child process."
- (let ((process-environment process-environment))
- (setenv "EDITOR" with-editor-sleeping-editor)
- (make-process
- :name (concat "khard" (car args))
- :command (apply 'list khard-executable args)
- :buffer nil
- :filter filter
- :sentinel 'khard--process-sentinel)))
- (defun khard-delete (contact no-confirm)
- "Delete CONTACT, which is of the form (name . uid).
- When called interactively, prompt the user.
- If NO-CONFIRM is nil, do not ask the user."
- (interactive (list (khard--prompt-contact "Delete Contact ") nil))
- (when (or no-confirm (yes-or-no-p (format "Really delete \"%s\"? "
- (car contact))))
- (khard--run-khard (list "delete" "--force"
- (format "uid:%s" (cdr contact))))))
- (defun khard--prompt-address-book ()
- "Prompt for an address book."
- (completing-read "Address Book " (process-lines "khard" "abooks")))
- (defun khard--new-process-filter (proc str)
- "Process filter for `khard-new' and `khard-edit'.
- PROC and STR are described in `set-process-filter'."
- (let ((lines (string-split str "\n"))
- (errors nil))
- (dolist (line lines)
- (cond
- ((string-prefix-p "Do you want to open the editor again? " line)
- (if (y-or-n-p (format "%sReopen the editor? "
- (cond
- ((null errors)
- "")
- ((length= errors 1)
- (concat (cl-first errors) ". "))
- (t
- (concat (string-join errors "\n") "\n")))))
- (process-send-string proc "y\n")
- (process-send-string proc "n\n")))
- ((string-match (rx bos "Error: " (group (+ any)) eol) line)
- (push (match-string 1 line) errors)))))
- (with-editor-process-filter proc str t))
- (defun khard-new (abook)
- "Create a new card and open it in an new buffer to edit.
- When called interactively, prompt for ABOOK."
- (interactive (list (khard--prompt-address-book)))
- (khard--run-khard (list "new" "--edit" "-a" abook)
- :filter 'khard--new-process-filter))
- (defun khard-edit (uid)
- "Edit the contact with UID.
- When called interactively, prompt the user."
- (interactive (list (cdr-safe (khard--prompt-contact "Edit Contact "))))
- (khard--run-khard (list "edit" "--edit" (format "uid:%s" uid))
- :filter 'khard--new-process-filter))
- (defun khard--parse-email-list (list-str)
- "Parse LIST-STR, a python dictionary and array string of emails."
- (if-let ((length (length list-str))
- ((>= length 2))
- (no-braces (substring list-str 1 -1)))
- (let ((output nil)
- (in-quote nil)
- (backslash nil)
- (in-value nil)
- (cur-str ""))
- (dotimes (i (- length 2))
- (let ((char (aref no-braces i)))
- (cond
- (in-quote
- (cond
- (backslash
- (setq cur-str (concat cur-str char)
- backslash nil))
- ((= char ?\\)
- (setq backslash t))
- ((= char ?')
- (push cur-str output)
- (setq cur-str ""
- in-quote nil))
- (t
- (setq cur-str (concat cur-str (list char))))))
- ((and in-value (= char ?'))
- (setq in-quote t))
- ((= char ?\[)
- (setq in-value t))
- ((= char ?\])
- (setq in-value nil)))))
- output)))
- (defun khard--make-email-contacts-list ()
- "Make a list of email contacts from khard."
- (let ((lines (process-lines "khard" "ls"
- "--parsable" "--fields=name,emails"))
- (output nil))
- (dolist (line lines)
- (cl-destructuring-bind (name email-list)
- (split-string line "\t")
- (dolist (email (khard--parse-email-list email-list))
- (push (format "%s <%s>"
- name
- email)
- output))))
- (seq-uniq output)))
- (defun khard--contacts-cache (&optional force)
- "Return the contacts cache, building it if nessesary.
- With FORCE, rebuild the cache no matter what."
- (when (or force (not khard--contacts-cache))
- (setq-local khard--contacts-cache (khard--make-email-contacts-list)))
- khard--contacts-cache)
- (defun khard-insert-email-contact ()
- "Use `completing-read' to prompt for and insert a khard contact."
- (interactive)
- (if-let (contact (completing-read "Insert Contact "
- (khard--contacts-cache t)))
- (insert contact)))
- (defun khard--message-in-header-p (name &optional testfn)
- "If in field NAME, return the start of the header, otherwise, return nil.
- The name is compared with the field name using TESTFN (defaults to `equal')."
- (save-excursion
- (when (and (message-point-in-header-p)
- (message-beginning-of-header t))
- (beginning-of-line)
- (when (and (looking-at (rx bol (group (+? any)) ":" (? " ")))
- (funcall (or testfn 'equal) (match-string 1) name))
- (match-end 0)))))
- (defun khard-message-mode-capf ()
- "Completion at point function for khard contacts in message mode."
- (interactive)
- (when-let ((field-start (khard--message-in-header-p "To")))
- (save-excursion
- (let ((end (point)))
- (re-search-backward (rx (any "\n" "," ":") (* whitespace))
- field-start t)
- (list (match-end 0) end (khard--contacts-cache))))))
- (defun khard-refresh-contact-cache (all-buffers &optional no-refresh)
- "Refresh the khard contact cache.
- When ALL-BUFFERS is non-nil, as it is with a prefix argument, refresh the cache
- of all buffers. With NO-REFRESH, don't refresh the cache, just clear it."
- (interactive "P")
- (let ((new-cache (and (not no-refresh) (khard--make-email-contacts-list))))
- (if all-buffers
- (cl-loop for buf being the buffers do
- (setf (buffer-local-value 'khard--contacts-cache buf)
- new-cache))
- (setq-local khard--contacts-cache new-cache))))
- (provide 'khard)
- ;;; khard.el ends here
|