potato face
CryingPotato

Exercise Logging in Obsidian

#obsidian

I use Obsidian to track all my workouts as a simple entry in my "Daily Note". These entries are then queryable with Dataview to create historical views of workouts with various filters applied. My tracking is a simple text entry in the file:

exercise:: squats - 190 - 5,5,5,3

With the format being roughly: exercise name, sets, reps. I often put additional notes in there, like what settings I use on a machine. Every time I start a workout I have a rough sense in my head of what exercises I plan to do, but I don't really remember what weights I should be using. Thus starts the processing of scrolling through my daily notes to find the last time I did this exercise, and using that as a baseline. I maintain a set of "weekly notes" that summarize this, but I haven't been good about generating these in the past few weeks, and it's still not ideal to have to scroll back and forth between the exercise entry and the weekly note.

So what do we want? Ideally, I want to enter the current exercise I'm doing. I should then see the past few times I've done this exercise, along with weight and rep ranges. I'll enter the current weight and reps I'm doing as I perform the exercise. When I'm done, I want to "click a button" and have that logged to my daily note in the same format as before.

To implement this search functionality, I turned to Dataview. I created a new note at notes/forever/exercise that had 3 properties: c_exercise, c_weight, c_reps - where the c_ prefix stands for current. I then wrote up a Dataview query (after a lot of trial and error) to search in a given window for matching exercises:

```dataviewjs

const windowInDays = 30;
const windowInMs = 1000 * 60 * 60 * 24 * windowInDays;
const today = new Date();
function matches(entry, query) {
if (entry.includes(query)) return true;

// check if all the individual words in the query are in the exercise
const queryWords = query.split(' ');
return queryWords.every(word => entry.includes(word))
}
const allExerciseEntries = Array.from(dv.pages('"daily"').filter(p => {
const pDate = new Date(`${p.file.name}`);
return (today - pDate) < windowInMs;
}).flatMap(p => {
let exercise = []
if (Array.isArray(p.exercise)) {
exercise = p.exercise
} else if (typeof p.exercise === 'string') {
exercise = [p.exercise]
}
return Array.from(exercise).map(e => [p.file.name, e])
}))
const { c_exercise } = dv.pages(`"${this.currentFilePath}"`)[0];
let rows = []
if (c_exercise) {
for (const entry of allExerciseEntries) {
if (matches(entry[1], c_exercise)) {
rows.push(entry)
}
}
}
rows = rows.sort((a, b) => new Date(b[0]) - new Date(a[0]))
dv.table(["Date", "Exercise"], rows)


```

Some notes about the above code snippet:

This worked but the table was really slow to update when I changed the query string. After some digging, I realized that Dataview has a default update window of 2.5 seconds after a file is changed. Dropping this down to 100ms globally made things work really smoothly!

To actually log the exercise entry I used a QuickAdd capture with an exercise template. The exercise template was really simple:

<%*
const dv = this.app.plugins.plugins["dataview"].api;
const filePath = "notes/forever/exercise";
const { c_exercise, c_weight, c_reps } = dv.pages(`"${filePath}"`)[0];
tR += `${c_exercise} - ${c_weight || 'n/a'} - ${c_reps || 'n/a'}`
%>

You can then configure QuickAdd to add this entry to your existing note with a capture format of exercise:: {{TEMPLATE:notes/forever/exercise.md}}.

All this worked really well on my laptop, but I ran into syncing hell when trying to get it on my iPhone. The first problem was that the exercise capture was being overwritten by an old capture definition I had on my phone - I fixed this by just entering the template definition on my phone. The other problem that I still haven't fixed is the template file sometimes gets overwritten with the template data - a spooky bug for a spooky time.

A few things I want to do in the future: