Through the Heart of Every Man — Through the Heart of Every Man
home

Through the Heart of Every Man

Tracking Physical Performance, Personal Library with Emacs

1 November 2023 | Emacs Fitness Data Forms.el

Table of Contents

I've just started making systematic efforts in my physical fitness. Following some work I've seen on periodized hypertrophy training, I planned out a mesocycle in my usual Android Notes, but noticed my systematic representation of workout data could much more ergonomically be represented as a forms file. The data entry is better, and data analysis is easier! Realizing just how good it is, I also implemented some basic library management in it for my physical books.

Forms Mode is a little-known, built-in corner of Emacs that makes reading and writing delimiter-separated value data a much more pleasant experience. It enables the end-user to create a text-based interface to the data, using a more primitive skeleton-like system to produce a buffer for each row in the file. Each field is editable, but the ambient text (that perhaps explains the semantics of the data fields) is read-only. I've found all its shortcomings reducible to those of delimiter-separated value data itself; for applications where that suffices, it really is an excellent tool.

1. Building a Forms File

To show and not tell, here's the forms control file I've whipped up to track exercises. It's slightly more complicated than is customary, because of the metaprogramming needed to economically implement the repeated per-exercise structure (this is one of those shortcomings of delimiter-separated value data).

;; Increase if you want, but it's probably not a good biomechanical idea.
(setq max-exercises 10)

(setq forms-file "lifts.tsv" ;; Name of the file the data is to be stored in.

      ;; Specially-interpreted list, describing the textual interface to the data.
      forms-format-list
      `("====== Lift Session ======\n\n"
        ;; Basic info.
        type " day, on " date ".\n"
        "Block " block ", mesocycle " mesocycle
        ", week " week ".\n\n"

        "Exercise, (sets)x(reps) weight—improvement made. Notes.\n"
        ,@(apply
           'append
           (mapcar (lambda (ex-number)
                     (let ((name     (intern (concat "ex" (number-to-string ex-number) "-name")))
                           (sets     (intern (concat "ex" (number-to-string ex-number) "-sets")))
                           (reps     (intern (concat "ex" (number-to-string ex-number) "-reps")))
                           (weight   (intern (concat "ex" (number-to-string ex-number) "-weight")))
                           (improved (intern (concat "ex" (number-to-string ex-number) "-improved")))
                           (notes    (intern (concat "ex" (number-to-string ex-number) "-notes"))))
                       (list name ", " sets"x"reps " " weight "—" improved ". " notes "\n")))
                   (number-sequence 1 max-exercises)))

        "\nAftermath:\n"
        aftermath)

      ;; The numbers of each field is associated with a corresponding symbol, for use in the above.
      forms-number-of-fields
      (forms-enumerate
       `(type
         date
         block
         mesocycle
         week
         ,@(apply
            'append
            (mapcar (lambda (ex-number)
                      (let ((name     (intern (concat "ex" (number-to-string ex-number) "-name")))
                            (sets     (intern (concat "ex" (number-to-string ex-number) "-sets")))
                            (reps     (intern (concat "ex" (number-to-string ex-number) "-reps")))
                            (weight   (intern (concat "ex" (number-to-string ex-number) "-weight")))
                            (improved (intern (concat "ex" (number-to-string ex-number) "-improved")))
                            (notes    (intern (concat "ex" (number-to-string ex-number) "-notes"))))
                        (list name sets reps weight improved notes)))
                    (number-sequence 1 max-exercises)))
         aftermath))

      ;; Bookkeeping about the data file; various unused data integrity checking features.
      forms-field-sep "\t"
      forms-read-only nil
      forms-multi-line ""
      forms-read-file-filter nil
      forms-write-file-filter nil
      forms-new-record-filter nil
      forms-insert-after nil
      forms-check-number-of-fields t)

Here's the interface:

lift.png

I will make a part two to this once I've finished a mesocycle or training block, showcasing the analysis I end up doing.

Here's the one for the library:

(setq forms-file "library.tsv"
      forms-format-list '("====== Library Book ======\n\n"
                          ;; Basic info.
                          "\"" title "\""
                          " by " author ".\n"
                          "Description:\n" description "\n"
                          "\n\n"
                          ;; Publication information.
                          "Edition " edition
                          (if (string-equal (nth volume forms-fields) "")
                              " "
                            ", volume ")
                          volume
                          (if (string-equal (nth volume forms-fields) "")
                              ""
                            "; ")

                          "published by " publisher ", "

                          (if (string-equal (nth series forms-fields) "")
                              ""
                            "in the ")
                          series
                          (if (string-equal (nth series forms-fields) "")
                              ""
                            " series, ")

                          "in " year "."
                          "\n\n"
                          ;; Physical information.
                          "Hardcover: " hardcover? "\n"
                          "Jacket: " jacket? "\n"
                          condition " condition; "
                          page-count " pages.\n"
                          "Last location: " location
                          "\n\n"
                          ;; Classification information.
                          "ISBN-13: " isbn-13
                          "\nLC Classification: " lc
                          ;; If you're the type to download a car,
                          ;; maybe put things like shadow archive hashes here too.
                          "\n\n"
                          ;; My relationship with it.
                          "Reading status " status ": "
                          (cond ((string-equal (nth status forms-fields) "0") "On the wishlist.")
                                ((string-equal (nth status forms-fields) "1") "Unread.")
                                ((string-equal (nth status forms-fields) "2") "Skimmed a little.")
                                ((string-equal (nth status forms-fields) "3") "Read at surface level.")
                                ((string-equal (nth status forms-fields) "4") "Read a good chunk in depth.")
                                ((string-equal (nth status forms-fields) "5") "Completely mastered.")
                                (t ""))

                          "\nBought from " purchase-from
                          ", " purchase-date ".\n"
                          "Opinion:\n" opinion)
      forms-number-of-fields (forms-enumerate
                              '(title
                                author
                                publisher
                                series
                                edition
                                volume
                                year
                                page-count
                                condition
                                hardcover?
                                jacket?
                                location
                                isbn-13
                                lc
                                status
                                purchase-from
                                purchase-date
                                opinion
                                description))
      forms-field-sep "\t"
      forms-read-only nil
      forms-multi-line ""
      forms-read-file-filter nil
      forms-write-file-filter nil
      forms-new-record-filter nil
      forms-insert-after nil
      forms-check-number-of-fields t)

To use these, just plop the control file in a directory, call M-x forms-find-file on it, and enter your data! TAB and S-TAB move forward and backward through field locations for editing; several normal Emacs keybindings operate at a meta level if prefixed by C-c , e.g. C-c C-n and C-c C-p move forward and backward through the records, and you can search the records for text with C-c C-s and C-c C-r. C-c C-o inserts a new record.

2. Data Analysis

The data stored may be consumed by analysis workflows in whatever language you want, within Emacs, with the great power of Org Babel. If you create an Org file in the same directory as the data and control files, you can put in a code block e.g.

set terminal png
set output "plot.png"

set xlabel "X Axis"
set ylabel "Y Axis"
plot "forms.tsv"

The resulting image can be embedded directly in the buffer by linking to it, resulting in dynamically-updating visualizations upon re-evaluating the code block! Intermediate computational results can be shared between snippets of different programming languages; analysis can be done in R, Python, and really most other programming languages you might want to use.

3. Distributed Entry

Currently, the biggest shortcoming of this approach to tracking is that it's limited by the provenance of Emacs. However, with Emacs (possibly) coming to Android in release 30, this is no longer a restriction, and you can enter data from your phone no problem. Synchronizing the data is as simple as tossing them under Git version control, and setting the remote to your VPS with a public IP.

I have a project going that currently translates form control files to Scheme, which I use to generate the library book list for this website. My hope is, eventually, to re-implement much of the forms.el functionality using Guile Hoot, and enable browser data entry to forms files! The source for this can be found among the source for this website (see the page footer).