Populate a Daily Habits List

I have a list of a few daily habits that I want to maintain. Updating this manually every day in TaskPaper was repetitive and error-prone, so I wrote a script that will populate the children of a dedicated “habits” project with my daily tasks. It will replace any entries under the project with the full list of habits, so each day I start with the whole list of habits by running this command.

I’m not sure if this is too niche of an enhancement, but in case it’s useful to others, here it is. You can customize the name of your habits project and the specific habits you want by adjusting the variables at the top of the script. A future enhancement might pull the habits list from a template file on your Mac instead of hardcoding it into the script.

Here’s the script:

const PARENT_PROJECT = "Daily Goals";

const HABITS = [
  "Meditate",
  "Exercise",
  "Language Practice",
];

function loadHabits(editor, options) {
  'use strict';

  const outline = editor.outline;

  outline.groupUndoAndChanges(function() {
    let projectMatches = outline.evaluateItemPath(`project ${options.parent_project}:`);

    if( projectMatches.length == 0 ) {
      // habits project doesn't exist, so create it
      outline.root.insertChildrenBefore(outline.createItem(`${options.parent_project}:`), outline.root.firstChild);
      projectMatches = outline.evaluateItemPath(`project ${options.parent_project}:`);
    }

    const dailyProject = projectMatches[0];
    dailyProject.removeChildren(dailyProject.children);

    dailyProject.appendChildren(
      options.habits.map(habit => outline.createItem(`- ${habit} @today`))
    );
  });
}

Application("TaskPaper").documents[0].evaluate({
  script: loadHabits.toString(),
  withOptions: {parent_project: PARENT_PROJECT, habits: HABITS}
})
3 Likes

First of all, I love the script.

Maybe it could be a good idea to add a function that checks to see if that parent project is there and if it is not, then creates it. Just in case someone tries to run it and they don’t happen to have that parent project already there. Otherwise they may not understand why the script is failing.

Just a suggestion to improve the script with those that happen to find something like this useful.

This Script has been added to wiki

Really glad you like it!

That’s a great suggestion! I just updated the script to do that.

I use Typinator, but TextExpander could also be used.

Taking a look at this. Thanks @batkins !

FYI—the first time I run this in Apple Script Editor, it works well. The project and children appear at the root of the front TaskPaper document.

If I run it a second time, I get:

Ideas? I was just trying to test the replacement of entries.

Hi @Jim this seems to be a weird limitation of Apple Script Editor that applies to all TaskPaper scripts. If you just make a quick change to the script and undo it (like typing a space and then removing the space), Apple Script Editor clears out its cached data and the script will work. If you run the script twice from the TaskPaper command picker, it will work correctly.

1 Like

Verified as a workaround solution. Thanks!

Odd. I have never seen that behavior in the Script Editor before.

I believe it’s because i’m using the const keyword for variables that don’t change. If they were var or let, Script Editor would be okay with reassigning them.

Ahh. That makes sense. Thanks for the insight!

The problem here is that the JavaScript interpreter has a top level global name-space with state that (perhaps counter-intuitively) persists between runs.

(we notice this a bit less with let and var name bindings, because they can be re-written, but they can still trip us up, because (if they are not over-written) they can still have unexpected hangover values from a previous script run)

If we bind names of any kind (with const, let or var) in the global name space, we are, in a sense ‘polluting’ it for other scripts down-stream.

The usual solution is to avoid binding any names in the global space at all (it’s already full of system stuff), and instead making temporary name bindings that vanish when your script is done.

The most general and reusable way to do this is to wrap all your code in a private name-space of its own – an IEFE (immediately executed function expression):

(() => {
    "use strict";

   // Our code can go here, and any const or let names that we bind
   //  will not pollute the global namespace, or trip up later script runs.
    
})();

This part defines a function which is nameless, and has no input arguments:

(() => 'something')

Including braces makes it a function with its own private name-space for const and let definitions (var is generally deprecated now)

(() => {
  
     // A private name-space in here for const or let definitions

})

and adding the parentheses at the end immediately ‘calls’/‘executes’/‘evaluates’ our anonymous function

(() => {
    "use strict";

      // Values and effects defined in here

})();  // open and close parentheses  invoke the function immediately.
2 Likes

PS

to get a sense of just how busy and crowded the global name-space is,

(how collision-prone – the kind of place where it’s not even easy to find your own const or let names, let alone be sure that they don’t clash with things already there)

try running the following one-word script in Script Editor, and seeing what shows up in the Result panel:

this

if you skip the enclosing:

(() => { 
    // my code here  
})();

That very crowded global name-space is where you are putting your own definitions, and that’s where they will still be when another script runs, or this script runs again …

2 Likes

PPS, on what makes var more complex and tricky (less simple and predictable) than const and let, there’s an example here:

(I personally never use var, and very rarely use let – defaulting to const is the best rule of thumb)

2 Likes

Wow. Thank you for the class. This is the type of knowledge that can really make some code hard to figure out if you don’t remember it. Hope you and the family are doing well @complexpoint.

1 Like

PS, adding "use strict"; just switches on more informative error messages if anything goes unexpectedly.

1 Like