Life Tracker Plugin

Settings

name

description

name

Life Tracker

icon

theater_comedy

description

Track mood and other life data.

setting

Note Data Name

instructions

Capturing a mood rating
Trigger Quick Open and type "record" to pick Record Mood Level make a mood reading. No mouse required!

Or choose the option for this plugin in your Daily Jots choices, this will show up if you have yet to record a mood level for the current day.

Data record
When you first invoke the plugin, you'll be prompted for a name to use for the note where your mood ratings/notes should be stored. Henceforth, any time you trigger a mood capture, your data will be logged to the top of the table within your note.

Plans
Development cycles will be unpredictable, but immediate aspirations for this plugin include:
1. Generate graphs to parse out how this data varies over time.
2. Allow tracking life quantities besides mood level (and graph those other quantities as well)








The author of this plugin (me, the CEO of Amplenote 👋) subsequently released a Bullet Journal plugin that also records mood ratings into a table, plus asks some good questions to explore the answer. You may want to install that plugin instead of this one?



Entry: https://github.com/alloy-org/life-tracker/build/compiled.js


linkCode block

// Javascript updated 6/9/2024, 8:14:03 PM by Amplenote Plugin Builder from source code within "https://github.com/alloy-org/life-tracker/build/compiled.js"
(() => {
// lib/plugin.js
var plugin = {
// --------------------------------------------------------------------------------------
constants: {
DEFAULT_NOTE_DATA_NAME: "LifeTracker Data",
DEFAULT_NOTE_DATA_TAGS: ["data"],
MOOD_SECTION_NAME: "Mood level",
NOTE_DATA_SETTING_KEY: "Note Data Name"
},
// --------------------------------------------------------------------------------------
dailyJotOption: {
"Record mood level": {
async run(app) {
await this._queryRecordMoodLevel(app);
},
async check(app) {
const note = await this._fetchDataNote(app);
if (!note)
return false;
const tableMarkdown = await this._tableData(app, note, this.constants.MOOD_SECTION_NAME);
if (!tableMarkdown)
return false;
const todayString = (/* @__PURE__ */ new Date()).toLocaleDateString();
return !tableMarkdown.includes(todayString);
}
}
},
// --------------------------------------------------------------------------------------
appOption: {
"Record mood level": async function(app) {
await this._queryRecordMoodLevel(app);
}
},
// --------------------------------------------------------------------------------------
_queryRecordMoodLevel: async function(app) {
const noteDataName = await this._fetchNoteDataName(app);
const moodOptions = [1, 2, 3, 4, 5].map((value) => ({ value, label: value }));
const result = await app.prompt("Record your current state", { inputs: [
{ label: "Mood level", type: "radio", options: moodOptions },
{ label: "What happened in the time leading up to this rating?", type: "text" }
] });
const lifeDataNote = await this._fetchDataNote(app, { noteDataName });
await this._persistData(app, lifeDataNote, this.constants.MOOD_SECTION_NAME, result);
},
// --------------------------------------------------------------------------------------
_persistData: async function(app, note, sectionName, result) {
let existingTable = await this._tableData(app, note, sectionName);
if (!existingTable) {
await app.insertNoteContent(note, `# ${sectionName}
`);
existingTable = "";
}
;
let tableMarkdown = `# ${sectionName}
`;
tableMarkdown += `| **Date** | **Mood** | **Precipitating events** |
| --- | --- | --- |
`;
tableMarkdown += `| ${(/* @__PURE__ */ new Date()).toLocaleString()} | ${result[0]} | ${result[1]} |
`;
tableMarkdown += existingTable;
await app.replaceNoteContent(note, tableMarkdown, { heading: { text: sectionName, level: 2 } });
},
// --------------------------------------------------------------------------------------
async _tableData(app, note, sectionName) {
const content = await app.getNoteContent(note);
let existingTable = "";
if (content.includes(`# ${sectionName}`)) {
existingTable = await this._sectionContent(content, sectionName);
if (existingTable?.length) {
const tableRows = existingTable.split("\n");
while (tableRows.length) {
const row = tableRows.shift();
if (row.includes("**Date**")) {
break;
}
}
return tableRows.join("\n");
}
}
},
// --------------------------------------------------------------------------------------
_fetchDataNote: async function(app, { noteDataName = null } = {}) {
if (this._noteHandle)
return this._noteHandle;
noteDataName = noteDataName || await this._fetchNoteDataName(app);
const existingNote = await app.findNote({ name: noteDataName });
if (existingNote) {
this._noteHandle = existingNote;
return existingNote;
}
const note = await app.createNote(noteDataName, this.constants.DEFAULT_NOTE_DATA_TAGS);
this._noteHandle = note;
return note;
},
// --------------------------------------------------------------------------------------
_fetchNoteDataName: async function(app) {
let noteDataName = await app.settings[this.constants.NOTE_DATA_SETTING_KEY];
if (!noteDataName) {
const result = await app.prompt(
`Enter the name of the note in which you'd like to record life data (default is "${this.constants.DEFAULT_NOTE_DATA_NAME}")`,
{ inputs: [{ type: "text" }] }
);
const noteName = result[0] || this.constants.DEFAULT_NOTE_DATA_NAME;
noteDataName = noteName;
await app.setSetting(this.constants.NOTE_DATA_SETTING_KEY, noteDataName);
}
return noteDataName;
},
// --------------------------------------------------------------------------------------
// Return all of the markdown within a section that begins with `sectionHeadingText`
// `sectionHeadingText` Text of the section heading to grab, with or without preceding `#`s
// `depth` Capture all content at this depth, e.g., if grabbing depth 2 of a second-level heading, this will return all potential h3s that occur up until the next h1 or h2
_sectionContent(noteContent, headingTextOrSectionObject) {
console.debug(`_sectionContent()`);
let sectionHeadingText;
if (typeof headingTextOrSectionObject === "string") {
sectionHeadingText = headingTextOrSectionObject;
} else {
sectionHeadingText = headingTextOrSectionObject.heading.text;
}
try {
sectionHeadingText = sectionHeadingText.replace(/^#+\s*/, "");
} catch (err) {
if (err.name === "TypeError") {
throw new Error(`${err.message} (line 1054)`);
}
}
const { startIndex, endIndex } = this._sectionRange(noteContent, sectionHeadingText);
return noteContent.slice(startIndex, endIndex);
},
// --------------------------------------------------------------------------------------
// Return {startIndex, endIndex} where startIndex is the index at which the content of a section
// starts, and endIndex the index at which it ends.
_sectionRange(bodyContent, sectionHeadingText) {
console.debug(`_sectionRange`);
const sectionRegex = /^#+\s*([^#\n\r]+)/gm;
const indexes = Array.from(bodyContent.matchAll(sectionRegex));
const sectionMatch = indexes.find((m) => m[1].trim() === sectionHeadingText.trim());
if (!sectionMatch) {
console.error("Could not find section", sectionHeadingText, "that was looked up. This might be expected");
return { startIndex: null, endIndex: null };
} else {
const level = sectionMatch[0].match(/^#+/)[0].length;
const nextMatch = indexes.find((m) => m.index > sectionMatch.index && m[0].match(/^#+/)[0].length <= level);
const endIndex = nextMatch ? nextMatch.index : bodyContent.length;
return { startIndex: sectionMatch.index + sectionMatch[0].length + 1, endIndex };
}
}
// There are several other entry points available, check them out here: https://www.amplenote.com/help/developing_amplenote_plugins#Actions
// You can delete any of the insertText/noteOptions/replaceText keys if you don't need them
};
var plugin_default = plugin;
return plugin;
})()