What do you do when you're into both Emacs geeks and social
networking? Well, you build a really really weird contact management
tool, that's what!
One of the things I often need to do is filter my contacts for a
particular set of interests. I would have no idea how to do this in
Microsoft Outlook and other proprietary contact management systems.
Because Emacs is infinitely programmable, though, I can just hack it
in.
You'd expect the intersection of the set “emacs geek” and the set
“social networker” to be a null set or a singleton (me!). As it turns
out, there's at least one other geek in this space – hooray!
Paul Lussier's been bouncing all sorts of
crazy ideas off me, which explains all the weird
porridge-and-toe-nails posts of Emacs Lisp code on my blog lately.
He's responsible for my puttting together yesterday's LinkedIn
importer. Today, he wrote:
Then found your sacha/bbdb-search-tags stuff which
totally, completely rocks. I just wish I had the first inkling as to
how it worked :) Now, the question I have is: How can I use
sacha/bbdb-search-tags to search for entries which are tagged with one
label, but NOT with another? For example, I want to search on: (and
(taq eq “planner”) (not (tag eq “muse”)))
I'd completely forgotten about sacha/bbdb-search-tags! Anyway, I'd
been meaning to write a fancy alias management thing for a while now,
and this code does a reasonable job for me. I can now filter my
displayed records by arbitrary Lisp expressions, bringing me closer to
insane contact relationship management. I mean, c'mon…
;; M-x sacha/bbdb-filter-by-alias-function RET
;; (lambda (aliases) (and (member "planner" aliases)
;; (not (member "muse" aliases))))) RET
If I do this often enough, I might make up an easier syntax, but
lisp expressions work fine for me.
MWAHAHAHAHA!
Here's the code:
;;;_+ Mail aliases
;; Code for working with aliases
;; You can use "a" (bbdb-add-or-remove-mail-alias) in BBDB buffers to add
;; a mail alias to the current entry, or "* a" to add a mail alias to
;; all displayed entries.
;; Goal: Be able to specify ALIAS and ALIAS
;; M-x sacha/bbdb-filter-displayed-records-by-alias RET alias alias
;; Goal: Be able to specify ALIAS or ALIAS
;; C-u M-x sacha/bbdb-filter-displayed-records-by-alias RET alias alias
;; Goal: Be able to specify not ...
;; M-x sacha/bbdb-omit-displayed-records-by-alias RET alias alias
;; C-u M-x sacha/bbdb-omit-displayed-records-by-alias RET alias alias
(defun sacha/bbdb-filter-by-alias-match-all (query-aliases record-aliases)
"Return non-nil if all QUERY-ALIASES are in RECORD-ALIASES."
(let ((result t))
(while query-aliases
(unless (member (car query-aliases) record-aliases)
(setq query-aliases nil
result nil))
(setq query-aliases (cdr query-aliases)))
result))
(defun sacha/bbdb-filter-by-alias-match-any (query-aliases record-aliases)
"Return non-nil if any in QUERY-ALIASES can be found in RECORD-ALIASES."
(let (result)
(while query-aliases
(when (member (car query-aliases) record-aliases)
(setq query-aliases nil
result t))
(setq query-aliases (cdr query-aliases)))
result))
;; Moved this to a convenience function so that we don't
;; have to deal with invert and property splitting.
(defun sacha/bbdb-filter-by-alias (bbdb-records
alias-filter-function
query
&optional invert)
"Return only the BBDB-RECORDS that match ALIAS-FILTER-FUNCTION.
ALIAS-FILTER-FUNCTION should accept two arguments:
- QUERY, a list of keywords to search for
- aliases, a list of keywords from the record
If INVERT is non-nil, return only the records that do
not match."
(delq nil
(mapcar
(lambda (rec)
(if (funcall alias-filter-function
query
(split-string
(or (bbdb-record-getprop
(if (vectorp rec)
rec
(car rec))
propsym) "")
"[ \n\t,]+"))
(when (null invert) rec)
(when invert rec)))
bbdb-records)))
;; Splitting this into two functions because of interactive calling.
(defun sacha/bbdb-filter-displayed-records-by-alias (query &optional any)
"Display only records whose mail-aliases match QUERY.
If ANY is non-nil, match if any of the keywords in QUERY are
present.
See also `sacha/bbdb-omit-displayed-records-by-alias'."
(interactive (list
(let ((crm-separator " "))
(completing-read-multiple
"Mail aliases: "
(bbdb-get-mail-aliases)))
current-prefix-arg))
(when (stringp query)
(setq query (split-string query "[ \n\t,]+")))
(bbdb-display-records
(sacha/bbdb-filter-by-alias-by-function
(or bbdb-records (bbdb-records))
(if any
'sacha/bbdb-filter-by-alias-match-any
'sacha/bbdb-filter-by-alias-match-all)
query)))
;; Splitting this into two functions because of interactive calling.
(defun sacha/bbdb-omit-displayed-records-by-alias (query &optional any)
"Display only records whose mail-aliases do not match QUERY.
If ANY is non-nil, match if any of the keywords in QUERY are
present.
See also `sacha/bbdb-filter-displayed-records-by-alias'."
(interactive (list
(let ((crm-separator " "))
(completing-read-multiple
"Mail aliases: "
(bbdb-get-mail-aliases))
current-prefix-arg)))
(when (stringp query)
(setq query (split-string query "[ \n\t,]+")))
(bbdb-display-records
(sacha/bbdb-filter-by-alias-by-function
(or bbdb-records (bbdb-records))
(if any
'sacha/bbdb-filter-by-alias-match-any
'sacha/bbdb-filter-by-alias-match-all)
query
t)))
;;;_+ Advanced mail alias queries
;; Goal: Use complicated lambda expressions to filter displayed records
;; M-x sacha/bbdb-filter-by-alias-function RET
;; (lambda (aliases) (and (member "planner" aliases)
;; (not (member "muse" aliases))))) RET
;; Thanks to Paul Lussier for the suggestion!
(defun sacha/bbdb-filter-by-alias-function (bbdb-records
alias-filter-function)
"Return only the BBDB-RECORDS that match ALIAS-FILTER-FUNCTION.
ALIAS-FILTER-FUNCTION should accept one argument:
- aliases, a list of keywords from the record."
(interactive (list (or bbdb-records (bbdb-records))
(read t)))
(let (records)
(setq records
(delq nil
(mapcar
(lambda (rec)
(when (funcall alias-filter-function
(split-string
(or (bbdb-record-getprop
(if (vectorp rec)
rec
(car rec))
propsym) "")
"[ \n\t,]+"))
rec))
bbdb-records)))
(if (interactive-p) (bbdb-display-records records))
records))