diff --git a/elisp/khard.el b/elisp/khard.el new file mode 100644 index 0000000..1a9b3ec --- /dev/null +++ b/elisp/khard.el @@ -0,0 +1,176 @@ +;;; khard.el --- Emacs integration with khard +;;; Commentary: +;;; Code: + +(require 'with-editor) + +(add-to-list 'display-buffer-alist '("\\*khard output\\*" . (display-buffer-no-window))) + +(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 (not (seq-empty-p clean-items)) + (format " (%s)" + (string-join clean-items ", ")) + ""))) + +(defun khard--remove-leading-label (field) + "Remove a leading \"name: \" from FIELD." + (if-let (index (string-search ":" field)) + (substring field (+ index 2)) + field)) + +(defun khard--build-uid-email-phone-list () + "Build a list in the format (info . uid)." + (let ((lines (process-lines "khard" + "ls" + "--parsable" + "--fields=uid,name,email,phone"))) + (mapcar (lambda (line) + (let* ((fields (split-string line "\t")) + (uid (car fields)) + (name (cadr fields)) + (email (khard--remove-leading-label (caddr fields))) + (phone (khard--remove-leading-label (cadddr fields)))) + (cons (format "%s%s" + name + (khard--build-list-entry-detail email phone uid)) + uid))) + lines))) + +(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)) + (shell-command-set-point-after-cmd (process-buffer proc)) + (message "khard: %s." (substring status 0 -1)))) + +(defun khard-edit (uid) + "Edit the contact with UID. +When called interactively, prompt the user." + (interactive (list (cdr-safe (khard--prompt-contact "Edit Contact ")))) + (let ((with-editor-shell-command-use-emacsclient nil)) + (make-process :name "khard edit" + :command + `("khard" "edit" "--edit" + ,(format "uid:%s" (cdr-safe contact))) + :buffer nil + :filter #'with-editor-process-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-safe contact)))) + (make-process :name "khard delete" + :command + `("khard" "delete" "--force" + ,(format "uid:%s" (cdr-safe contact))) + :buffer nil + :sentinel #'khard--process-sentinel))) + +(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'. +PROC and STR are described in `set-process-filter'." + (let ((lines (string-split str "\n"))) + (dolist (line lines) + (cond + ((string-prefix-p "Error: " line) + (setq error-msg line)) + ((equal + "Do you want to open the editor again? (y/N) " + line) + (if (y-or-n-p (format "%s! Reopen the editor? " + (or error-msg + "Unknown error"))) + (process-send-string proc "y\n") + (process-send-string proc "n\n")))))) + (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))) + (when abook + (let ((error-msg nil)) + (make-process :name "khard new" + :command + `("env" ,(concat "EDITOR=" with-editor-sleeping-editor) + "khard" "new" "--edit" "-a" ,abook) + :buffer nil + :filter #'khard--new-process-filter + :sentinel #'khard--process-sentinel)))) + +(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 ?') + (add-to-list 'output cur-str) + (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) + (let* ((fields (split-string line "\t")) + (name (car fields)) + (email-list (cadr fields))) + (dolist (email (khard--parse-email-list email-list)) + (add-to-list 'output (format "%s <%s>" + name + email))))) + output)) + +(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--make-email-contacts-list))) + (insert contact))) + +(provide 'khard) +;;; khard.el ends here diff --git a/init.el b/init.el index f955099..66235ac 100644 --- a/init.el +++ b/init.el @@ -2,6 +2,9 @@ ;;; Commentary: ;;; Code: +;; Some other config files +(add-to-list 'load-path (expand-file-name "elisp")) + ;; Set package dir to follow no-littering conventions (setq package-user-dir "~/.emacs.d/var/elpa") @@ -375,6 +378,9 @@ visual states." ;; json (use-package json-mode) +;; yaml +(use-package yaml-mode) + ;; sly (use-package sly :hook (lisp-mode . my/common-lisp-autoconnect-sly) @@ -420,16 +426,21 @@ visual states." "S" #'magit-stage-modified)) ;; mu4e +(require 'auth-source-pass) +(auth-source-pass-enable) (add-to-list 'load-path "/usr/share/emacs/site-lisp/mu4e/") (require 'mu4e) (add-hook 'mu4e-index-updated-hook #'my/-mu4e-enable-index-messages) +(evil-define-key '(normal motion) mu4e-main-mode-map "q" #'bury-buffer) (defun my/-mu4e-enable-index-messages () (setq mu4e-hide-index-messages nil)) (defun my/mu4e-update-mail-and-index-silent () "Run `mu4e-update-mail-and-index' without any messages in the background." (setq mu4e-hide-index-messages t) (mu4e-update-mail-and-index t)) -(setq mu4e-change-filenames-when-moving t +(setq message-kill-buffer-on-exit t + message-send-mail-function 'sendmail-send-it + mu4e-change-filenames-when-moving t mu4e-context-policy 'pick-first mu4e-index-update-error-warning nil mu4e-get-mail-command "mbsync protonmail" @@ -459,6 +470,8 @@ visual states." (use-package mu4e-alert :after mu4e :hook (after-init . mu4e-alert-enable-notifications) + :init + (setq mu4e-alert-set-window-urgency nil) :config (mu4e-alert-set-default-style 'libnotify))