I've been using Emacs with org-mode to track my diet since 2012. I've had some breaks along the way, the overall setup has stayed the same.

I use this system to track how much I weigh, as well as how many calories I'm consuming in a single day. There are plenty of apps and online services that provide this functionality, but I prefer to own my data in an open format I can use elsewhere.

Seeing as I work from home and always have an Emacs session open, it made sense for me to try to utilize Emacs in some way.

My set up uses the following Emacs and org-mode functionality:

Let's take a closer look at how all of this fits together.

Diet file setup

My diet file looks like this (with headings collapsed):

#+TITLE: Diet Tracker
#+SEQ_TODO: CAL-IN | CAL-OUT CAL-CANCEL

* Daily Logs
** CAL-OUT Diet for day <2020-07-03 Fri>...
** CAL-OUT Diet for day <2020-07-02 Thu>...
** CAL-OUT Diet for day <2020-07-01 Wed>...
** CAL-OUT Diet for day <2020-06-30 Tue>...
** CAL-OUT Diet for day <2020-06-29 Mon>...
... And more

I tried to keep the file format as simple as possible. Each day has its own entry underneath the main "Daily Logs" header. The main "Daily Logs" header is used by org-capture to find where to place new items.

The top-level SEQ_TODO property is used to set the "done" state for each headline. I use three different types of headline:

CAL-IN
For days where I haven't entered all of my diet information. There's usually only one of these open at a time.
CAL-OUT
For finished days where all data is final.
CAL-CANCEL
For days where I'm tracking weight but not calorie information. I use this for Sundays, days where I go out to eat, or when I have social events planned.

Daily entries

A daily entry looks something like this:

** CAL-OUT Diet for day <2019-10-05 Sat>
  :PROPERTIES:
  :Weight:   167.3
  :END:

| Timestamp              | Food                   | Calories | Quantity |   Total |
|------------------------+------------------------+----------+----------+---------|
| [2019-10-05 Sat 08:42] | Cup of tea             |       54 |        1 |      54 |
| [2019-10-05 Sat 11:34] | Chocolate Chip Pancake |      2.7 |      202 |   545.4 |
| [2019-10-05 Sat 14:46] | Cup of tea             |       54 |        1 |      54 |
| [2019-10-05 Sat 14:46] | Vanilla yogurt raisins |     4.33 |       30 |   129.9 |
| [2019-10-05 Sat 17:45] | Beef meatballs         |     2.02 |      370 |   747.4 |
| [2019-10-05 Sat 17:45] | Fresh Pasta            |     1.31 |      306 |  400.86 |
| [2019-10-05 Sat 17:45] | Garlic bread           |     2.75 |       94 |   258.5 |
| [2019-10-05 Sat 19:58] | Cup of tea             |       54 |        1 |      54 |
|------------------------+------------------------+----------+----------+---------|
| Total                  |                        |          |          | 2244.06 |
#+TBLFM: $5=$3*$4::$LR5=vsum(@2$5..@-I$5)

It's a fairly simple table that uses some org-mode magic for calculating totals. The "Calories" column is usually "calories per gram", but for some items it's "calories per item". Likewise, the quantity column either refers to the weight in grams or the number of items consumed.

The #+TBFLM: part underneath the table is an org-mode spreadsheet formula. It uses two formulae:

$5=$3*$4
Sets column 5 (the "Totals" column) to Calories x Quantity. org-mode columns indexes start from 1 rather than 0.
$LR5=vsum(@2$5..@-I$5)
Calculates the total amount of calories consumed during the day. It uses relative references so that it works no matter how many lines

The Spreadsheet section of the org-mode manual goes into detail on formulas. It took me a while to get the hang of, but it's a really powerful system.

Weigh-ins

I use an org-capture-template for my weigh-ins. I weigh myself every morning, depending on my schedule.

org-capture is bound to C-c o r, and then my weigh-in template is bound to w. So every day I run C-c o r w, enter my weight, then use C-c C-c to save it to my diet file. And that's it.

My capture template is below:

(add-to-list
 org-capture-templates
 '("w" "Weigh-in" entry
   (file+headline "~/self/diet.org" "Daily Logs")
   "* CAL-IN Diet for day %t
%^{Weight}p
| Timestamp | Food | Calories | Quantity | Total |
|--------------+---+----------+----------+-------|
|--------------+---+----------+----------+-------|
| Total        |   |          |          |       |
#+TBLFM: $5=$3*$4::$LR5=vsum(@2$5..@-I$5)"
   :prepend t))

Adding new food entries

I have a couple of elisp functions I use for adding new data. The primary function is org-diet-copy, which is bound to C-c C-C.

org-diet-copy is used on a row of another table. It copies the food name, calorie amount, and quantity to the top table and replaces the timestamp with the current date and time.

The process of adding a new entry typically goes like this:

  • Hit C-s to search for the food I want to add. If I wanted to add a new entry for "french fries" I would probably do something like "C-s fren" to find the first "french fries" entry.
  • Hit C-c C-C to copy the entry to my active day.
  • Replace the quantity with whatever amount I ate. I try to keep the same portion size for breakfast and snacks, so this isn't always necessary.
  • Run M-x org-table-recalculate to update the table.

It's a simple system, but it works well enough. Eventually I may add function that prompts for a food and quantity, and then automatically fills in the calorie amount.

All of the functions I use are below:

(defun org-diet-move-today ()
  "Move to today's entry in the org-diet file."
  (interactive)
  (beginning-of-buffer)

  ;; Move to first heading (Daily Logs) and expand.
  (outline-next-visible-heading 1)
  (show-children)
  (outline-next-visible-heading 1))

(defun org-diet-move-last-entry ()
  "Move to the last entry in the current diet table."
  (interactive)

  (search-forward "#+TBLFM")

  ;; Move to start of final line.
  (previous-line 1)
  (previous-line 1)
  (move-beginning-of-line 1))

(defun org-diet-copy ()
  "Copy the current table line to today.

Copies the current table line and moves it to the bottom of
today's diet table.  Changes the timestamp to the current time
and day."
  (interactive)

  (let ((diet-line (thing-at-point 'line t)))
    ;; Jump to today & last line.
    (org-diet-move-today)
    (org-diet-move-last-entry)

    ;; Insert the copied line.
    (insert diet-line)

    ;; Move back to last line.
    (org-diet-move-last-entry)
    (forward-line -1)

    ;; Replace the date.
    (beginning-of-line)
    (search-forward "| ")
    (zap-to-char 1 93)
    (insert (format-time-string "[%Y-%m-%d %a %H:%M]" (current-time)))

    ;; Update the table.
    (forward-line 2)
    (org-table-recalculate)

    ;; Move to inserted line.
    (beginning-of-line)
    (forward-line -2)))

;; Bind copying to `C-c C-C`.
(global-set-key "\C-cC" #'org-diet-copy)

In part two I'll cover the system I use to extract data from the org-mode file. It's not pretty.