From 092304d9e0ccc37cc0ddaa9b136457e56a1cac20 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 12 Oct 2025 11:47:26 -0500 Subject: changing repositories --- modules/org-contacts-config.el | 205 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 modules/org-contacts-config.el (limited to 'modules/org-contacts-config.el') diff --git a/modules/org-contacts-config.el b/modules/org-contacts-config.el new file mode 100644 index 00000000..899df28d --- /dev/null +++ b/modules/org-contacts-config.el @@ -0,0 +1,205 @@ +;;; org-contacts-config.el --- Org Contacts Configuration -*- lexical-binding: t; coding: utf-8; -*- +;; author: Craig Jennings +;; +;;; Commentary: +;; Configuration for org-contacts, providing contact management within org-mode. +;; Integrates with mu4e for email address completion and org-roam for linking +;; contacts to projects and notes. +;; +;; Email completion functionality has been moved to mu4e-org-contacts-integration.el + +;;; Code: + +(require 'user-constants) + +;; --------------------------- Org Agenda Integration -------------------------- + +(with-eval-after-load 'org-agenda + ;; Remove the direct hook first (in case it's already added) + (remove-hook 'org-agenda-finalize-hook 'org-contacts-anniversaries) + + ;; Add a wrapper function that ensures proper context + (defun cj/org-contacts-anniversaries-safe () + "Safely call org-contacts-anniversaries with required bindings." + (require 'diary-lib) + ;; These need to be dynamically bound for diary functions + (defvar date) + (defvar entry) + (defvar original-date) + (let ((date (calendar-current-date)) + (entry "") + (original-date (calendar-current-date))) + (ignore-errors + (org-contacts-anniversaries)))) + + ;; Use the safe wrapper instead + (add-hook 'org-agenda-finalize-hook 'cj/org-contacts-anniversaries-safe)) + +;;; ---------------------------- Capture Templates ------------------------------ + +(with-eval-after-load 'org-capture + (add-to-list 'org-capture-templates + '("C" "Contact" entry (file+headline contacts-file "Contacts") + "* %(cj/org-contacts-template-name) + +Added: %U"))) + +(defun cj/org-contacts-template-name () + "Get name for contact template from context." + (let ((name (when (boundp 'cj/contact-name) cj/contact-name))) + (or name + (when (eq major-mode 'mu4e-headers-mode) + (mu4e-message-field (mu4e-message-at-point) :from-or-to)) + (when (eq major-mode 'mu4e-view-mode) + (mu4e-message-field mu4e~view-message :from-or-to)) + (read-string "Name: ")))) + +(defun cj/org-contacts-template-email () + "Get email for contact template from context." + (let ((email (when (boundp 'cj/contact-email) cj/contact-email))) + (or email + (when (eq major-mode 'mu4e-headers-mode) + (let ((from (mu4e-message-field (mu4e-message-at-point) :from))) + (when from (cdr (car from))))) + (when (eq major-mode 'mu4e-view-mode) + (let ((from (mu4e-message-field mu4e~view-message :from))) + (when from (cdr (car from))))) + (read-string "Email: ")))) + +;;; ------------------------- Quick Contact Functions --------------------------- + +(defun cj/org-contacts-find () + "Find and open a contact." + (interactive) + (find-file contacts-file) + (goto-char (point-min)) + (let ((contact (completing-read "Contact: " + (org-map-entries + (lambda () (nth 4 (org-heading-components))) + nil (list contacts-file))))) + (goto-char (point-min)) + (search-forward contact) + (org-fold-show-entry) + (org-reveal))) + +(defun cj/org-contacts-new () + "Create a new contact." + (interactive) + (org-capture nil "C")) + +(defun cj/org-contacts-view-all () + "View all contacts in a column view." + (interactive) + (find-file contacts-file) + (org-columns)) + +;;; -------------------------- Org-Roam Integration ----------------------------- + +;; (with-eval-after-load 'org-roam +;; (defun cj/org-contacts-link-to-roam () +;; "Link current contact to an org-roam node." +;; (interactive) +;; (when (eq major-mode 'org-mode) +;; (let ((contact-name (org-entry-get (point) "ITEM"))) +;; (org-set-property "ROAM_REFS" +;; (org-roam-node-id +;; (org-roam-node-read nil nil nil nil +;; :initial-input contact-name))))))) + +;;; ----------------------------- Birthday Agenda -------------------------------- + +(with-eval-after-load 'org-agenda + ;; Add birthdays to agenda + (setq org-agenda-include-diary t) + + ;; Custom agenda command for upcoming birthdays + (add-to-list 'org-agenda-custom-commands + '("b" "Birthdays and Anniversaries" + ((tags-todo "BIRTHDAY|ANNIVERSARY" + ((org-agenda-overriding-header "Upcoming Birthdays and Anniversaries") + (org-agenda-sorting-strategy '(time-up)))))))) + +;;; ---------------------------- Core Contact Data Functions --------------------------- + +(defun cj/org-contacts--props-matching (entry pattern) + "Return all property values from ENTRY whose keys match PATTERN (a regexp)." + (let ((props (nth 2 entry))) + (delq nil + (mapcar (lambda (prop) + (when (string-match-p pattern (car prop)) + (cdr prop))) + props)))) + +(defun cj/get-all-contact-emails () + "Retrieve all contact emails from org-contacts database. +Returns a list of formatted strings like \"Name \". +This is the core function used by the mu4e integration module." + (let ((contacts (org-contacts-db))) + (delq nil + (mapcan (lambda (e) + (let* ((name (car e)) + ;; This returns a LIST of email strings + (email-strings (cj/org-contacts--props-matching e "EMAIL"))) + ;; Need mapcan here to handle the list + (mapcan (lambda (email-str) + (when (and email-str (string-match-p "[^[:space:]]" email-str)) + (mapcar (lambda (email) + (format "%s <%s>" name (string-trim email))) + (split-string email-str "[,;[:space:]]+" t)))) + email-strings))) + contacts)))) + +;; Simple insertion function for use outside of mu4e +(defun cj/insert-contact-email () + "Select and insert a contact's email address at point. +For use outside of mu4e compose buffers. In mu4e, the integration +module provides more sophisticated completion." + (interactive) + (let* ((items (cj/get-all-contact-emails)) + (selected (completing-read "Contact: " items nil t))) + (insert selected))) + +;;; -------------------------------- Org Contacts -------------------------------- + +(use-package org-contacts + :after (org mu4e) + :custom + (org-contacts-files (list contacts-file)) + :config + (require 'mu4e) + ;; Basic settings + (setq org-contacts-icon-use-gravatar nil) ; Don't fetch gravatars + + ;; Birthday and anniversary handling + (setq org-contacts-birthday-format "It's %l's birthday today! 🎂") + (setq org-contacts-anniversary-format "%l's anniversary 💑") + + ;; Email address formatting + (setq org-contacts-email-link-description-format "%s <%e>") + + (setq mu4e-org-contacts-file contacts-file) + (add-to-list 'mu4e-headers-actions + '("org-contact-add" . mu4e-action-add-org-contact) t) + (add-to-list 'mu4e-view-actions + '("org-contact-add" . mu4e-action-add-org-contact) t) + + ;; Disable mu4e's built-in completion in favor of our custom solution + (setq mu4e-compose-complete-addresses nil)) + +;;; ---------------------------- Org-Contacts Keymap ---------------------------- + +;; Keymap for `org-contacts' commands +(defvar cj/org-contacts-map + (let ((map (make-sparse-keymap))) + (define-key map "f" 'cj/org-contacts-find) ;; find contact + (define-key map "n" 'cj/org-contacts-new) ;; new contact + (define-key map "e" 'cj/insert-contact-email) ;; inserts email from org-contact + (define-key map "v" 'cj/org-contacts-view-all) ;; view all contacts + map) + "Keymap for `org-contacts' commands.") + +;; Bind the org-contacts map to the C-c C prefix +(global-set-key (kbd "C-c C") cj/org-contacts-map) + +(provide 'org-contacts-config) +;;; org-contacts-config.el ends here \ No newline at end of file -- cgit v1.2.3