Category Archives: pimpmyemacs

More Emacs goodness: Refresh your memory when you e-mail using notes from BBDB

Inspired by an e-mail-based customer relationship management system briefly described by Daniel Charles of digital ketchup at Shoeless Joe's last Friday, I decided to hack together a system that would allow me to see the notes from my contact database (aptly named the Big Brother Database, or BBDB) when I write e-mail using the Gnus mail client in Emacs.

The first thing I needed to build, of course, was something that removed my notes from outgoing messages. People really don't need to see the kinds of notes I keep on them. ;) Well, they're fairly innocuous notes: how we met and what they're interested in, usually, although sometimes I'll have notes on people's food preferences or shoe sizes. I've recently started keeping track of the subjects of e-mail I send them, too.

(defun sacha/gnus-remove-notes ()
  "Remove everything from --- NOTES --- to the signature."
  (goto-char (point-min))
  (when (re-search-forward "^--- NOTES ---" nil t)
    (let ((start (match-beginning 0))
          (end (and (re-search-forward "^--- END NOTES ---") (match-end 0))))
      (delete-region start end))))
(add-hook 'message-send-hook 'sacha/gnus-remove-notes)

Then it was easy to write another function that composed individual messages to all the people currently displayed in the BBDB buffer, adding notes to each message.

(defun sacha/gnus-send-message-to-all (subject)
  "Compose message to everyone, with notes."
  (interactive "MSubject: ")
  (let ((records bbdb-records))
    (while records
      (when (bbdb-record-net (caar records))
        (bbdb-send-mail (caar records) subject)
        (when (bbdb-record-notes (caar records))
            (insert "\n--- NOTES ---\n"
                    (bbdb-record-notes (caar records))
                    "\n--- END NOTES ---\n"))))
      (setq records (cdr records)))))

I use BBDB to display only the people I want to e-mail, then I call M-x sacha/gnus-send-message-to-all and specify a message subject. This creates a gazillion message buffers which I can then edit. If I feel particularly paranoid, I can remove the notes section myself with C-c C-z (message-kill-to-signature), but sacha/gnus-remove-notes does it as long as it's in message-send-hook.

This code works particularly well with these other customizations:

It supersedes More Emacs fun: Composing mail to everyone with notes.

On Technorati: , , , , ,


Emacs: Changing the font size on the fly

I have a tiny laptop: 8.9" diagonally. With a 1024x768 pixels screen resolution, things can get *pretty* small. The following functions use the gnome-terminal-style shortcuts (Ctrl-plus, Ctrl-minus) to change the font size without the mouse:

(defun sacha/increase-font-size ()
  (set-face-attribute 'default
                      (ceiling (* 1.10
                                  (face-attribute 'default :height)))))
(defun sacha/decrease-font-size ()
  (set-face-attribute 'default
                      (floor (* 0.9
                                  (face-attribute 'default :height)))))
(global-set-key (kbd "C-+") 'sacha/increase-font-size)
(global-set-key (kbd "C--") 'sacha/decrease-font-size)

On Technorati: ,

Emacs clinic at the Linux Caffe

Quinn Fung needed some help with Muse and RDF so that she could easily generate RSS feeds from Emacs, so we declared today to be Emacs Clinic day at the Linux Caffe.

We started by getting publishing to work. We then figured out how to get RDF to publish, and that was pretty okay too.

Quinn needed multiple authors, and muse-journal didn't support it yet, so we hacked it in. I told her to pick a syntax, and I added code to make it happen. It took us a while to track things down, but it turned out to be a reasonably easy addition. (I need to refactor that code sometime... that's a really long function!)

Along the way, we found a bug in muse-journal. Muse-journal summarizes entries by taking the first two sentences, but dies when the post doesn't contain at least two periods. I spent a fair bit of time tracing through the different changes we made before realizing that it wasn't my bug. I probably would've found it earlier, but debug-on-error wasn't getting honored. Odd. Anyway, here's the patch, which I'll submit to GNA when I get back into the swing of things:

--- orig/lisp/muse-journal.el
+++ mod/lisp/muse-journal.el
@@ -570,7 +570,9 @@
           (let ((beg (point)))
             (if (muse-style-element :summarize)
-                  (forward-sentence 2)
+                  (condition-case err
+                      (forward-sentence 2)
+                    (error (goto-char (point-min))))
                   (setq desc (concat (buffer-substring beg (point)) "...")))
                 (muse-publish-markup-buffer "rss-entry" "html")

Now Quinn's jumping feet-first into Lisp development by doing the Atom implementation. muse-atom does single-entry Atom files, but she can model it on muse-journal's RSS implementation.

I also helped Ian set up a very very basic Planner. It reminded me that I *really* need to package planner-bundle again, and either retire or update plannerlove. In fact, I need to set up scripts so that it's ridiculously easy to keep up to date...

I miss hacking on Emacs! This is fun. I'll reconfigure my kernel and get VPN working. Then I'll set up my Emacs development environment again...

On Technorati: ,

Emacs + LinkedIn: Another totally idiosyncratic bit of code

The following code should not be run until you've backed up your Big Brother Database and sacrificed a chicken. It goes through the list of people in your exported LinkedIn CSV, creates BBDB records if necessary, adds a linkedin mail alias, and notices new e-mail addresses and job titles. Call sacha/linkedin-import from the CSV. Needs csv.el and lookout.el, which you should load before running this code.

If anyone else ever finds this useful, I'll be quite surprised.

(require 'csv)
(require 'lookout)

(setq lookout-bbdb-mapping-table
      '(("lastname" "Last Name")
        ("firstname" "First Name")
        ("company" "Company")
        ("job" "Job Title")
        ("net" "E-mail Address")))

(defun sacha/lookout-bbdb-check-linkedin (line)
  (let* ((lastname  (lookout-bbdb-get-value "lastname" line))
	 (firstname (lookout-bbdb-get-value "firstname" line))
	 (company   (lookout-bbdb-get-value "company" line))
         (job       (lookout-bbdb-get-value "job" line))
	 (net       (lookout-bbdb-get-value "net" line))
	 (addr1     (lookout-bbdb-get-value "addr1" line))
	 (addr2     (lookout-bbdb-get-value "addr2" line))
	 (addr3     (lookout-bbdb-get-value "addr3" line))
	 (phones    (lookout-bbdb-get-value "phones" line t)) ;; !
	 (notes     (lookout-bbdb-get-value "notes" line ))
         (j (concat job ", " company))
	 (otherfields (lookout-bbdb-get-value "otherfields" line t))
	 (addrs nil)
         (n (concat "^" firstname " " lastname))
	 (record (or (bbdb-search (bbdb-records) n)
                     (bbdb-search (bbdb-records) nil nil net)))
	 (message ""))
    (unless record
      (if (string= company "") (setq company nil))
      (if (string= notes "") (setq notes nil))
      (if (and addr1 (> (length addr1) 0))
	  (add-to-list 'addrs (vector "Address 1" (list addr1) "" "" "" "")))
      (if (and addr2 (> (length addr2) 0))
	  (add-to-list 'addrs (vector "Address 2" (list addr2) "" "" "" "")))
      (if (and addr3 (> (length addr3) 0))
	  (add-to-list 'addrs (vector "Address 3" (list addr3) "" "" "" "")))
      (setq record (list
                    (lookout-bbdb-create-entry (concat firstname " " lastname)
                                               (concat job ", " company)
    ;; Check if net has changed
    (when record
      (setq record (car record))
      (let ((nets (bbdb-record-net record)))
        (unless (member net nets)
          ;; New e-mail address noticed, add to front of list
          (add-to-list 'nets net)
          (bbdb-record-set-net record nets)
          (message "%s %s: New e-mail address noticed: %s" firstname lastname net)))
      ;; Check if job title and company have changed
      (when (or job company)
         ((string= (or (bbdb-record-company record) "") "")
          (bbdb-record-set-company record j))
         ((string= (bbdb-record-company record) j)
           (concat "Noticed change from job title of "
                   (bbdb-record-company record)
           (bbdb-record-notes record)))
          (message "%s %s: Noticed change from job title of %s to %s"
                   firstname lastname (bbdb-record-company record) j)
          (bbdb-record-set-company record j))))
      (let* ((propsym bbdb-define-all-aliases-field)
             (oldaliases (bbdb-record-getprop record propsym)))
        (if oldaliases (setq oldaliases
                             (if (stringp oldaliases)
                                 (bbdb-split oldaliases ",")
        (add-to-list 'oldaliases "linkedin")
        (setq oldaliases (bbdb-join oldaliases ", "))
        (bbdb-record-putprop record propsym oldaliases)))))

(defun lookout-bbdb-create-entry (name company net addrs phones notes
				       &optional otherfields)
  (when (or t (y-or-n-p (format "Add %s to bbdb? " name)))
    ;;(message "Adding record to bbdb: %s" name)
    (let ((record (bbdb-create-internal name company net addrs phones notes)))
      (unless record (error "Error creating bbdb record"))
      (mapcar (lambda (i)
		(let ((field (make-symbol (aref i 0)))
		      (value (aref i 1)))
		  (when (and value (not (string= "" value)))
		    (bbdb-insert-new-field record field value))))

(defun lookout-bbdb-get-value (key entry &optional as-vector-list)
  "Returns the value for a key from a lispified csv line, using the mapping
  (let* ((table (if (listp lookout-bbdb-mapping-table)
		  (symbol-value lookout-bbdb-mapping-table)))
	 (mapped-keys (cdr (assoc key table)))
	 (result nil)
	 (separator ""))

    (unless as-vector-list
      (setq result ""))
    (when mapped-keys
      (if (stringp mapped-keys)
          (setq mapped-keys (list mapped-keys)))
      (mapcar (lambda (i)
                ;;(message "%s...%s" i (cdr (assoc i entry)))
                (let ((value (cdr (assoc i entry))))
                  (unless (string= "" value)
                    (if as-vector-list
                        (add-to-list 'result (vector i value))
                      (setq result (concat result separator value)))
                    (setq separator " "))))
    ;;(message "%s" result)

(defun sacha/linkedin-import ()

On Technorati: , , ,

Emacs: Show only people whom I haven’t pinged since…

One of the things I want in a contact management system is a quick way to find out who I haven't pinged in a while. The following code filters currently-displayed contacts to show who I might want to get back in touch with. Call it from a *BBDB* window and specify the date (could be 2006.01.01 for annual, -7 for the last seven days, etc.). This works incredibly well with the following hacks:

I should write a small book about how to build a contact management system with Emacs. ;) It's insanely powerful, you know.

(require 'planner)
(require 'bbdb)
(defun sacha/bbdb-show-only-no-contact-since (date)
  "Show only people who haven't been pinged since DATE or at all."
  (interactive (list (planner-read-date)))
  (let ((records bbdb-records)
    (while records
      ;; Find the latest date mentioned in the entry
      (setq notes (or (bbdb-record-notes (caar records)) ""))
      (setq last-match nil omit nil)
      (while (string-match
              (or last-match 0))
        (unless (string> date (match-string 0 notes))
          (setq omit t)
          (setq last-match (length notes)))
        (setq last-match (match-end 0)))
      (unless (and last-match omit)
        (add-to-list 'new-records (caar records) t))
      (setq records (cdr records)))
    (bbdb-display-records new-records)))

One of the other things I'd like to smooth over is keeping track of who owes whom e-mail... <laugh>

On Technorati: , , , , , ,

Emacs: BBDB rapid serial visualization

And because it's good to quickly flash through records once in a while to refresh my memory...

(defvar sacha/bbdb-rapid-serial-visualization-delay
 "*Number of seconds to wait between records.
Set to 0 to wait for input.")

(defun sacha/bbdb-rapid-serial-visualization ()
  "Breeze through everyone's name and notes."
  (window-configuration-to-register ?a)
  ;; Copy the currently visible records
  (let ((records bbdb-records)
        (default-size (face-attribute 'default :height))
        (new-size 400)
        (continue t))
    (set-face-attribute 'default nil :height new-size)
    (pop-to-buffer (get-buffer-create "BBDB-Serial"))
    (while (and records continue)
      (insert (bbdb-record-name (caar records))
              (or (car (bbdb-record-net (caar records))) "No e-mail")
              (or (bbdb-record-notes (caar records)) "")
              (make-string 50 ?\n))
      (goto-char (point-min))
      (sit-for sacha/bbdb-rapid-serial-visualization-delay)
      (setq records (cdr records)))
    (set-face-attribute 'default nil :height default-size)
    (when continue
      (jump-to-register ?a))))

On Technorati: , , , ,