Complete Computing Environment: Managing My Life Sanely

Table of Contents

(provide 'cce-home)

I can hit C in my showme hydra to get dropped in to a list of world clocks, which is incredibly useful when my coworkers and the systems I support are active 24/7/365.

(setq display-time-world-list '(("America/Los_Angeles" "Oakland")
                                ("UTC" "UTC")
                                ("America/New_York" "New York")
                                ("Europe/Amsterdam" "Amsterdam")
                                ("Europe/Copenhagen" "Denmark")
                                ("EET" "Bulgaria")
                                ("Asia/Shanghai" "China")
                                ("Asia/Calcutta" "India")))

And of course, knowing what the weather is in the morning is also quite useful. I can see the weather by hitting w or W in my showme hydra

(install-pkgs '(wttrin))
(setq wttrin-default-cities '("Oakland" "San Francisco" "Phoenix"))

Accounting with Ledger and Org-mode

Ledger is a neat accounting tool that I am trying out right now. I've got some org-mode babel code that I'll need to pull in to here to pull transactions out of my Simple Bank activity and parse them in to Ledger format files that can be worked with.

(install-pkgs '(ledger-mode flycheck-ledger dklrt))
rpm-install ledger
deb-install ledger

I'm still working on useful reporting, but here are a few that I've grabbed so far. They're just simple reports that show me this month's budget, as well as things that I haven't budgeted out yet.

(eval-after-load 'ledger-mode (lambda ()
                                (add-to-list
                                 'ledger-reports
                                 '("unbudgeted" "ledger -f %(ledger-file) -p \"last 30 days\" --monthly balance ^expenses --unbudgeted"))
                                (add-to-list
                                 'ledger-reports
                                 '("budget" "ledger -f %(ledger-file) -p \"last 30 days\" --monthly balance ^expenses --budget"))
                                (add-to-list
                                 'ledger-reports
                                 '("bills" "ledger -f %(ledger-file) reg ^expenses:recurring:* -p \"last 30 days\""))))


I have an Org capture template to add Ledger entries. This one is pretty cool, because it uses prompts and other fun features that people often overlook in capture templates.

(add-to-list 'org-capture-templates '("l" "Ledger Entry" plain (file "~/org/inbox.ledger")
               "%<%Y/%m/%d> %^{To whom}
    Assets:Checking  $ -%^{How much}
    Expenses:%?" :unnarrowed t) t)

Contact Management

I use the Insidious Big Brother Database1 for contact management. It's easy to use, incredibly powerful, and easy to build on top of. There's a bunch of code on EmacsWiki2 and on Sacha Chua's blog3 on how to build cool things with BBDB, and I'm only scratching the surface. Unfortunately, a lot of the code that is out there is built to focus on BBDB2 which is old and outdated. It's not hard to port these things to bbdb3, so I do where I can, and include them here.

One thing I need to figure out is how to slurp coworkers' information in to a place that Gnus can complete on them; the ability to load multiple bbdb indexes would be ideal here, obviously, and I could set up a script which munges the company's LDAP system in to a .bbdb suitable to query on.

(install-pkgs '(bbdb bbdb-ext bbdb2erc bbdb-vcard))
(bbdb-initialize 'gnus 'mail 'message 'pgp 'anniv)

When I start a phone call, I can hit C-c P to dive in to a capture template, and can then hit P inside hydra-workflow to be able to insert a BBDB entry quickly.

(defun bh/phone-call ()
  "Return name and company info for caller from bbdb lookup"
  (interactive)
  (require 'bbdb)
  (require 'bbdb-com)
  (let* (name rec caller)
    (setq name (completing-read "Who is calling? "
                                bbdb-hashtable
                                'bbdb-completion-predicate
                                'confirm))
    (when (> (length name) 0)
      ; Something was supplied - look it up in bbdb
      (setq rec
            (or (first
                 (or (bbdb-search (bbdb-records) name nil nil)
                     (bbdb-search (bbdb-records) nil name nil)))
                name)))

    ; Build the bbdb link if we have a bbdb record, otherwise just return the name
    (setq caller (cond ((and rec (vectorp rec))
                        (let ((name (bbdb-record-name rec))
                              (company (first (bbdb-record-organization rec))))
                          (concat "[[bbdb:"
                                  name "]["
                                  name "]]"
                                  (when company
                                    (concat " - " company)))))
                       (rec)
                       (t "NameOfCaller")))
    (insert caller)))

This code from Sacha's BBDB configuration will let you easily add a contact entry to a BBDB entry:

(defun sacha/bbdb-ping-bbdb-record (bbdb-record text &optional date regrind)
  "Adds a note for today to the current BBDB record.
Call with a prefix to specify date."
  (interactive (list (bbdb-current-record t)
                     (read-string "Notes: ")
                     (format-time-string "%Y-%m-%d")
                     t))
  (bbdb-record-set-field (first bbdb-record)
                         'contact
                         (concat date ": " text "\n"
                                 (or (bbdb-record-field bbdb-record 'contact))))
  (if regrind
      (save-excursion
        (set-buffer bbdb-buffer-name)
        (bbdb-redisplay-record bbdb-record)))
  nil)

I leverage these functions to make it easy to attach a Org heading to a contact:

(defun rrix/attach-org-heading-to-bbdb-record (id record desc)
  (interactive (list (org-id-get-create)
                     (bbdb-completing-read-record "Attach to whom? ")
                     (org-get-heading)))
  (sacha/bbdb-ping-bbdb-record (list record) (concat "[[" id "]] - " desc) (format-time-string "%Y-%m-%d")))

All three together gives me this sort of workflow for things like phone calls:

  • Get a call
  • Hit C-c P
  • Begin taking notes
  • Hit <hydra-workflow> p to fill in the caller's bbdb card
  • Call M-x rrix/attach-org-heading-to-bbdb-record to create the inverse relationship, and the ability to show me any notes attached to that person, for the next time I need to care about them.

Let's always make sure we can do tab-completion on bbdb contacts.

(add-hook 'message-mode-hook 'bbdb-mail-aliases 'append)

Setting up bbdb-auto-notes-rules allows you to classify people based on the mail you receive from them, including adding arbitrary heads like Organization and that sort of thing. Quite powerful, I'm just using a configuration mimed from Sacha's old-ish bbdb configuration4 on EmacsWiki.

(setq bbdb-auto-notes-rules '(("To" ("emacsconf" . "emacsconf")
                                    ("emacs" . "emacs")
                                    ("gnus" . "emacs")
                                    ("org-mode" . "emacs")
                                    ("fedoraproject" . "fedora")
                                    ("kde.org" . "kde")
                                    ("ry@n.rix.si" . "personal mail")
                                    ("ryan@whatthefuck.copmuter" . "personal mail")
                                    ("rrix@uber.com" . "work email"))
                              ("From" ("uber.com" . "work email")
                                      ("fedoraproject" . "fedora")
                                      ("kde.org" . "kde"))
                              ("Organization" (".*" organization 0 nil))
                              ("X-Face" (".+" face 0 'replace)) ))

This code from EmacsWiki adds a binding : in EWW to add the current URL to a contact, as in a person's home page or blog. Just a nice little way to make that flow easier.

(unless (version< emacs-version "24.4")
  (eval-after-load 'eww-mode (lambda ()
                               (define-key eww-mode-map ":" 'sdl-bbdb-www-grab-homepage-eww)

                               (defun sdl-bbdb-www-grab-homepage-eww (record)
                                 "Grab the current URL and store it in the bbdb database"
                                 (interactive (list (bbdb-completing-read-record
                                                     "Add WWW homepage for: ")))
                                 ;; if there is no database record for this person, create one
                                 (unless record
                                   (setq record (bbdb-read-record)))
                                 (if (bbdb-record-field record 'www)
                                     (bbdb-record-set-field
                                      record 'www
                                      (concat (bbdb-record-field record 'www) "," eww-current-url))
                                   (bbdb-record-set-field record 'www eww-current-url))
                                 (bbdb-change-record record t)
                                 (bbdb-display-records (list record))))))

More code from EmacsWiki, this one shows an X-face if you have one for a contact (or import it! using bbdb-auto-notes-rules).

(add-hook 'bbdb-list-hook 'my-bbdb-display-xface)
(defun my-bbdb-display-xface ()
  "Search for face properties and display the faces."
  (when (or (gnus-image-type-available-p 'xface)
            (gnus-image-type-available-p 'pbm))
    (save-excursion
      (goto-char (point-min))
      (let ((inhibit-read-only t); edit the BBDB buffer
            (default-enable-multibyte-characters nil); prevents corruption
            pbm faces)
      (while (re-search-forward "^           face: \\(.*\\)" nil t)
        (setq faces (match-string 1))
        (replace-match "" t t nil 1)
        (dolist (data (split-string faces ", "))
          (setq pbm (uncompface data))
          (if (gnus-image-type-available-p 'xface)
              (insert-image
               (gnus-create-image
                (concat "X-Face: " data)
                'xface t :ascent 'center :face 'gnus-x-face))
            (when pbm
              (insert-image
               (gnus-create-image
                pbm 'pbm t :ascent 'center :face 'gnus-x-face))))
          (insert " ")))))))

This function ports bbdb-ext5 to be compatible with bbdb3, specifically its ability to show a google maps page from a contact's addresses. It's quite nice, when it works ;)

(defun bbdb-gm-address (rec)
  "Get the address that will be used by google maps for REC.
If there is no address filed for rec, `nil' will be returned.
If there are several addresses for REC, the address nearset point will be used."
  (let ((addresses (bbdb-record-address rec)))
    (when addresses
      (save-excursion
    (let ((prop (bbdb-current-field))
          (p (point))
          (i 0))
      (while (and prop (not (eq 'name (car prop))))
        (bbdb-next-field -1)
        (setq prop (bbdb-current-field)))
      (while (<= (point) p)
        (setq prop (bbdb-current-field))
        (if (eq 'address (car prop))
        (progn
          ;; For some records, `bbdb-next-field' doesn't work properly
          ;; when (= 2 (length prop)) and `bbdb-next-field' is called
          ;; it doesn't move to the next field, it's still in the same record
          ;; but (= 3 (length prop)).
          ;; So when it's an address field, (= 2 (length prop)) marks a real
          ;; address field.
          (if (= 2 (length prop))
              (setq i (1+ i)))))
        (bbdb-next-field 1))
      (if (zerop i)
          (car addresses)
        (nth (1- i) addresses)))))))

This hook will add a note to any bbdb entity that I email, with the date and subject of when I've emailed them, in the contact field.

(defun sacha/gnus-add-subject-to-bbdb-record ()
  "Add datestamped subject note for each person this message has been sent to."
  (require 'message)
  (require 'bbdb-gnus)
  (let* ((subject (concat (format-time-string "%Y-%m-%d")
                          ": E-mail: " (message-fetch-field "Subject") "\n"))
         (bbdb-message-all-addresses t)
         records)
    (setq records
          (bbdb-update-records (bbdb-get-address-components 'recipients 'no) t))
    (mapc (lambda (rec)
            (bbdb-record-set-field rec
                                   'contact
                                   (concat subject "\n"
                                           (or (bbdb-record-field rec 'contact))))
            (bbdb-change-record rec)
            (message "Added a Contact entry for %s." (bbdb-record-field rec 'name)))
          records)))

(eval-after-load 'message-mode (lambda ()
                                 (add-hook 'message-send-hook 'sacha/gnus-add-subject-to-bbdb-record)))

The Inventory

I am attempting to build a full inventory of everything I own. It's half a reason to figure out what I use, and what I don't use so that I can minimize and sell/donate things that I don't use, and half as an insurance buffer to know what I lost in some sort of catastrophe scenario (file server disk failure, natural disaster)

I use, naturally, Org mode to build out my inventory. In essence, I have an org_archive file with two headings, a list of Locations where things live, and a list of things. Each thing has a bunch of properties which I can use to filter and calculate on.

I have a capture template which makes it easy to add newly purchased things in to the inventory. This capture template uses some of the neater features of org-capture-templates, namely evaluating and inserting arbitrary s-exps and prompts.

(add-to-list 'org-capture-templates '("v" "inventory item" entry (file+headline "~/org/inventory.org_archive" "Things")
                                      "** %(print eww-current-title) %? :UNCATEGORIZED:
:PROPERTIES:
:LOCATION: %^{LOCATION}p
:QUANTITY: %^{QUANTITY}p
:VALUE: %^{VALUE}p
:ACQUIRED_ON: %^t
:URL: %l
:END:" :clock-in f))

I use this for new things that I purchase, the workflow is basically to open the product page for the thing I purchased in eww and hit <hydra-workflow> c v to capture it to my inVentory. This will then ask me a number of questions, where I will put it, how many I got, the cash value, and then store that under my Things heading in The Inventory.

I also have an interactive function which will capture a region, convert it to an inventory entry and then re-insert a link to that entry. What this allows me to do is to quickly type out a thing that I have, such as when I am working on a taskflow, and replace that with a link to the item quickly.

(defun rrix/capture-region-to-inventory (location value quantity)
  "Kill the region and replace it with a link to the captured
heading. LOCATION VALUE and QUANTITY will be inserted in to their
respective PROPERTIES table entries"
  (interactive "sWhere is it? \nsHow much is it worth? \nsHow many do you have? ")
  (kill-region (mark) (point))
  (with-current-buffer (find-file "/home/rrix/org/inventory.org_archive")
    (goto-char (point-max))
    (insert (format  "\n** %s :UNCATEGORIZED:
:PROPERTIES:
:LOCATION: %s
:VALUE: %s
:QUANTITY: %s
:END:\n
"
                     (car kill-ring)
                     location
                     value
                     quantity))
    (org-id-get-create)
    (org-store-link nil))
  (org-insert-link))

I have a helm function which will allow me to quickly jump to any Inventory item. I can use this to quickly find where an item of mine is located, by jumping to the item and then opening its properties. I need to rewrite it to use completing-read or something.

;(defun rrix/helm-org-inventory-files-headings ()
;  "Quickly jump to an Inventory Archive heading"
;  (interactive)
;  (helm :sources (helm-source-org-headings-for-files '("/home/rrix/org/inventory.org_archive"))
;        :candidate-number-limit 99999
;        :buffer "*helm org inventory headings*"))

This function uses org-map-entries to pull a table of inventory statistics.

(defun rrix/inventory-stats ()
  (interactive)
  (let ((counts (make-hash-table :test 'equal))
        (today (format-time-string "%Y-%m-%d" (current-time)))
        values output
        (total 0)
        (entries 0))
    (org-map-entries
     (lambda ()
       (let* ((props (org-entry-properties (point) 'all))
              (compo (org-heading-components))
              (status (elt compo 2))
              (prio (elt compo 3))
              (tagstring (cdr (assoc "TAGS" props)))
              (taglist (if tagstring (split-string tagstring ":")))
              (alltagstring (cdr (assoc "ALLTAGS" props)))
              (alltaglist (if alltagstring (split-string alltagstring ":")))
              (value (cdr (assoc "VALUE" props)))
              (quantity (cdr (assoc "QUANTITY" props)))
              (last-used (cdr (assoc "LAST_USED" props)))
              (acquired-on (cdr (assoc "ACQUIRED" props))))
         (unless (-contains? tagstring "CATEGORY") ; Skip CATEGORYs
           (setq total (1+ total))
           (when quantity
             (setq entries (+ (string-to-number quantity) entries)))
           (when value
             (puthash "TOTALVAL" (+ (* (string-to-number (or quantity "1"))
                                       (string-to-number value))
                                    (or (gethash "TOTALVAL" counts) 0)) counts))
           (when (and last-used
                      (< (time-to-seconds (date-to-time last-used)) (gethash "UNUSED" counts)))
             (puthash "UNUSED" (time-to-seconds (date-to-time last-used)) counts))
           (when (and acquired-on
                      (< (time-to-seconds (date-to-time acquired-on)) (gethash "OLDEST" counts)))
             (puthash "OLDEST" (time-to-seconds (date-to-time acquired-on)) counts))
           (when status
             (puthash status (1+ (or (gethash status counts) 0)) counts))
           (when prio
             (puthash prio (1+ (or (gethash prio counts) 0)) counts))
           (unless prio
             (puthash "NP" (1+ (or (gethash "NP" counts) 0)) counts))
           (when alltaglist
             (mapc (lambda (tag)
                     (puthash tag (1+ (or (gethash tag counts) 0)) counts))
                   alltaglist)))))
     nil
     '("~/org/inventory.org_archive"))
    (setq values (mapcar (lambda (x)
                           (or (gethash x counts) 0))
                         '("TOTALVAL" 65 66 67 "NP" "CONSUMABLE" "CLOTHING" "PREPAREDNESS" "ATTACH" "UNCATEGORIZED" "UNUSED" "OLDEST")))
    (setq output
          (concat "| " today " | "
                  (mapconcat (lambda (n)
                               (number-to-string
                                (round n))) values " | ")
                  " | "
                  (number-to-string entries)
                  " | "
                  (number-to-string total)
                  " | "
                  (number-to-string
                   (round (* 100.0 (/ (gethash "UNCATEGORIZED" counts) (float total)))))
                  "% |"))
    (if (called-interactively-p 'any)
        (insert output)
      output)))

I have a bunch of helpers, as well, to aide in printing labels for objects. I use a DYMO LabelManager PnP working on GNU/Linux with CUPS and the documentation provided here. This all goes well with GLabels' support for mail merge; I create a two-item CSV, one line of ID,ITEM and one with, well, ID and the name, and it prints little DYMO labels that I can attach to objects. Soon I'll whip up a little OCR thing for my phone that will find the ID, and then ask an Emacs instance over Matrix for more information about the object.

(defun cce/capture-to-inventory-and-print ()
  "Capture a new object in to the inventory and print a label for it.

This function captures an org-mode entry using the 'v' template,
and then, by abusing `org-capture-after-finalize-hook', goes to
that object's header and prints that object using glabels/lp."
  (interactive)
  (setq cce/old-org-capture-after-finalize-hook org-capture-after-finalize-hook
        org-capture-after-finalize-hook '((lambda ()
                                            (debug)
                                            (setq org-capture-after-finalize-hook
                                                  cce/old-org-capture-after-finalize-hook)
                                            (org-capture-goto-last-stored)
                                            (cce/print-inventory-at-point))))
  (org-capture 4 "v"))

(defun cce/print-inventory-at-point ()
  "Print a label for the inventory item at point."
  (interactive)
  (cce/print-inventory
   (org-id-get-create)
   (elt (org-heading-components) 4)))

(defun cce/print-inventory (id name)
  (shell-command (format "echo ID,ITEM > /home/rrix/org/fuck.csv"))
  (shell-command (format "echo \"%s,%s\" >> /home/rrix/org/fuck.csv" id name))
  (shell-command (format "glabels-3-batch -o /tmp/%s.pdf ~/org/inventory.glabels" id))
  (async-shell-command (format "lp -d DYMO_LabelManager_PnP /tmp/%s.pdf" id)))

I still have quite a few wants/needs, mostly related to finding "old" items:

  • A function to set a last-used-date property on a given heading
  • A function to query the last-used-date of a heading
  • A function to query headings that have a last-used-date before a given date

External Memory

In essence, my computer serves as my external memory. It augments my brain to remember the things I've read, the places I've been to, the things I need to do, the thoughts that I've had in the past. It's kind of scary just how much information about me is spread around on this machine and others. The information and metadata exists and is useful, I think it's worth trying to centralize it and unify an interface for it, while also minimizing the leakage of this data as much as possible to third parties.

Memacs as Zeitgeist

Memacs6 is an interesting piece of kit that basically takes a bunch of data formats and spits out an org mode file representing that data and their metadata. Sounds pretty simple, but the effect, when you throw a bunch of sources in to it, is pretty impressive. The "Example Story" they provide is a bit contrived, but even in the short time that I had it enabled, just feeding it RSS from my github, git logs for all the main repositories I work on, my twitter and my email inbox, was all pretty useful. I have plans to extend this to do even more, in the long term, as I decide more what is useful for me.

Wanted Extensions:

  • Photo metadata
  • Email metadata
  • Git logs
    • All work projects
    • Personal Ansible repositories
    • Personal dotfiles repository
  • Call logs from N900
  • SMS from N900
  • Browser History

ELKStack

ElasticSearch7 is a really nice piece of kit; bolt a RESTful API and decent query DSL on top of Apache Solar and build some kit that makes it easy to throw data in to it via Logstash8. Then build a nice frontend for it for web browsing with Kibana9. The effect is impressive, this toolkit is becoming a standard for any sort of operations enterprise, and I am looking at sane ways to fit it in to my personal life.

Emacs has a decent, if out of date, interface library with es.el10; I should update it or write a new one. And then feed a bunch of shit in to a private Logstash:

  • IRC Logs
  • Email meta data
  • Twets

Further, as mentioned above, more and more I am using Elasticsearch within my job, as a way to visualize, aggregate and search logs. Having the ability to intuitively pull ElasticSearch data in to e.g. Org-mode babel files or to process them as buffers would be a wonderful opportunity.

INPROGRESS Org as Zeitgeist for life

Footnotes:

Author: Ryan Rix

Created: 2017-05-19 Fri 13:29

Validate XHTML 1.0