summaryrefslogtreecommitdiff
path: root/org/lift-form/lift-form.org~
blob: 712b1fc6f12c4c9ff59930bacca838a5f82148b9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# -*- org-export-use-babel: nil;-*-

#+TITLE: Tracking Physical Performance, Personal Library with Emacs
#+DATE: <2023-11-01 Wed 00:42>
#+TAGS: Emacs, Fitness, Data, Forms.el

I've just started making systematic efforts in my physical fitness. Following some [[https://rpstrength.com/hypertrophy-training-guide-central-hub/][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.

[[https://www.gnu.org/software/emacs/manual/html_mono/forms.html][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 [[https://www.gnu.org/software/emacs/manual/html_node/autotype/index.html][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.

* 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).

#+begin_src emacs-lisp :eval never-export

  ;; 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)

#+end_src

Here's the one for the library:

#+begin_src emacs-lisp :eval never-export

  (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)

#+end_src

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.

* 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.

* 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, e.g.,


That's all!