khard.el 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. ;;; khard.el --- Emacs integration with khard -*- lexical-binding: t -*-
  2. ;;; Commentary:
  3. ;;; Code:
  4. (require 'with-editor)
  5. (require 'cl-lib)
  6. (require 'message)
  7. (defcustom khard-executable "khard"
  8. "The executable to use to run khard."
  9. :group 'khard
  10. :type 'string)
  11. (defvar-local khard--contacts-cache ()
  12. "List of contacts used while completing at point.
  13. This exists so that Emacs doesn't slow down while running
  14. `completion-at-point-functions'. This is local to each buffer.")
  15. (defun khard--build-list-entry-detail (&rest items)
  16. "Build a detail in the format \" (ITEMS)\", or an empty string."
  17. (let ((clean-items (remove "" items)))
  18. (if clean-items
  19. (format " (%s)" (string-join clean-items ", "))
  20. "")))
  21. (defun khard--build-uid-email-phone-list ()
  22. "Build a list in the format (info . uid)."
  23. (cl-loop for line in
  24. (process-lines "khard" "ls"
  25. "--parsable" "--fields=uid,name,email,phone")
  26. for (uid name email phone) = (split-string line "\t")
  27. collect
  28. (cons (format "%s%s" name
  29. (khard--build-list-entry-detail email phone uid))
  30. uid)))
  31. (defun khard--prompt-contact (&optional prompt)
  32. "Prompt user for a contact, optionally make the prompt text PROMPT."
  33. (if-let ((uid-list (khard--build-uid-email-phone-list))
  34. (resp (completing-read (or prompt "Contact ") uid-list)))
  35. (assoc resp uid-list)))
  36. (defun khard--process-sentinel (proc status)
  37. "Process sentinel for kahrd commands.
  38. For info on PROC and STATUS, see `set-process-sentinel'."
  39. (when (memq (process-status proc) '(exit signal))
  40. (message "khard: %s." (substring status 0 -1))))
  41. (cl-defun khard--run-khard (args &key filter)
  42. "Run khard with ARGS.
  43. FILTER is a process filter to install on the child process."
  44. (let ((process-environment process-environment))
  45. (setenv "EDITOR" with-editor-sleeping-editor)
  46. (make-process
  47. :name (concat "khard" (car args))
  48. :command (apply 'list khard-executable args)
  49. :buffer nil
  50. :filter filter
  51. :sentinel 'khard--process-sentinel)))
  52. (defun khard-delete (contact no-confirm)
  53. "Delete CONTACT, which is of the form (name . uid).
  54. When called interactively, prompt the user.
  55. If NO-CONFIRM is nil, do not ask the user."
  56. (interactive (list (khard--prompt-contact "Delete Contact ") nil))
  57. (when (or no-confirm (yes-or-no-p (format "Really delete \"%s\"? "
  58. (car contact))))
  59. (khard--run-khard (list "delete" "--force"
  60. (format "uid:%s" (cdr contact))))))
  61. (defun khard--prompt-address-book ()
  62. "Prompt for an address book."
  63. (completing-read "Address Book " (process-lines "khard" "abooks")))
  64. (defun khard--new-process-filter (proc str)
  65. "Process filter for `khard-new' and `khard-edit'.
  66. PROC and STR are described in `set-process-filter'."
  67. (let ((lines (string-split str "\n"))
  68. (errors nil))
  69. (dolist (line lines)
  70. (cond
  71. ((string-prefix-p "Do you want to open the editor again? " line)
  72. (if (y-or-n-p (format "%sReopen the editor? "
  73. (cond
  74. ((null errors)
  75. "")
  76. ((length= errors 1)
  77. (concat (cl-first errors) ". "))
  78. (t
  79. (concat (string-join errors "\n") "\n")))))
  80. (process-send-string proc "y\n")
  81. (process-send-string proc "n\n")))
  82. ((string-match (rx bos "Error: " (group (+ any)) eol) line)
  83. (push (match-string 1 line) errors)))))
  84. (with-editor-process-filter proc str t))
  85. (defun khard-new (abook)
  86. "Create a new card and open it in an new buffer to edit.
  87. When called interactively, prompt for ABOOK."
  88. (interactive (list (khard--prompt-address-book)))
  89. (khard--run-khard (list "new" "--edit" "-a" abook)
  90. :filter 'khard--new-process-filter))
  91. (defun khard-edit (uid)
  92. "Edit the contact with UID.
  93. When called interactively, prompt the user."
  94. (interactive (list (cdr-safe (khard--prompt-contact "Edit Contact "))))
  95. (khard--run-khard (list "edit" "--edit" (format "uid:%s" uid))
  96. :filter 'khard--new-process-filter))
  97. (defun khard--parse-email-list (list-str)
  98. "Parse LIST-STR, a python dictionary and array string of emails."
  99. (if-let ((length (length list-str))
  100. ((>= length 2))
  101. (no-braces (substring list-str 1 -1)))
  102. (let ((output nil)
  103. (in-quote nil)
  104. (backslash nil)
  105. (in-value nil)
  106. (cur-str ""))
  107. (dotimes (i (- length 2))
  108. (let ((char (aref no-braces i)))
  109. (cond
  110. (in-quote
  111. (cond
  112. (backslash
  113. (setq cur-str (concat cur-str char)
  114. backslash nil))
  115. ((= char ?\\)
  116. (setq backslash t))
  117. ((= char ?')
  118. (push cur-str output)
  119. (setq cur-str ""
  120. in-quote nil))
  121. (t
  122. (setq cur-str (concat cur-str (list char))))))
  123. ((and in-value (= char ?'))
  124. (setq in-quote t))
  125. ((= char ?\[)
  126. (setq in-value t))
  127. ((= char ?\])
  128. (setq in-value nil)))))
  129. output)))
  130. (defun khard--make-email-contacts-list ()
  131. "Make a list of email contacts from khard."
  132. (let ((lines (process-lines "khard" "ls"
  133. "--parsable" "--fields=name,emails"))
  134. (output nil))
  135. (dolist (line lines)
  136. (cl-destructuring-bind (name email-list)
  137. (split-string line "\t")
  138. (dolist (email (khard--parse-email-list email-list))
  139. (push (format "%s <%s>"
  140. name
  141. email)
  142. output))))
  143. (seq-uniq output)))
  144. (defun khard--contacts-cache (&optional force)
  145. "Return the contacts cache, building it if nessesary.
  146. With FORCE, rebuild the cache no matter what."
  147. (when (or force (not khard--contacts-cache))
  148. (setq-local khard--contacts-cache (khard--make-email-contacts-list)))
  149. khard--contacts-cache)
  150. (defun khard-insert-email-contact ()
  151. "Use `completing-read' to prompt for and insert a khard contact."
  152. (interactive)
  153. (if-let (contact (completing-read "Insert Contact "
  154. (khard--contacts-cache t)))
  155. (insert contact)))
  156. (defun khard--message-in-header-p (name &optional testfn)
  157. "If in field NAME, return the start of the header, otherwise, return nil.
  158. The name is compared with the field name using TESTFN (defaults to `equal')."
  159. (save-excursion
  160. (when (and (message-point-in-header-p)
  161. (message-beginning-of-header t))
  162. (beginning-of-line)
  163. (when (and (looking-at (rx bol (group (+? any)) ":" (? " ")))
  164. (funcall (or testfn 'equal) (match-string 1) name))
  165. (match-end 0)))))
  166. (defun khard-message-mode-capf ()
  167. "Completion at point function for khard contacts in message mode."
  168. (interactive)
  169. (when-let ((field-start (khard--message-in-header-p "To")))
  170. (save-excursion
  171. (let ((end (point)))
  172. (re-search-backward (rx (any "\n" "," ":") (* whitespace))
  173. field-start t)
  174. (list (match-end 0) end (khard--contacts-cache))))))
  175. (defun khard-refresh-contact-cache (all-buffers &optional no-refresh)
  176. "Refresh the khard contact cache.
  177. When ALL-BUFFERS is non-nil, as it is with a prefix argument, refresh the cache
  178. of all buffers. With NO-REFRESH, don't refresh the cache, just clear it."
  179. (interactive "P")
  180. (let ((new-cache (and (not no-refresh) (khard--make-email-contacts-list))))
  181. (if all-buffers
  182. (cl-loop for buf being the buffers do
  183. (setf (buffer-local-value 'khard--contacts-cache buf)
  184. new-cache))
  185. (setq-local khard--contacts-cache new-cache))))
  186. (provide 'khard)
  187. ;;; khard.el ends here