Script: move a @now or @next tag on to the next item

I use a @now tag as a bookmark to the task I am working on.

(TextBar displays the tagged task (with some parental/ancestral context) on the OS X menubar – see Script: displaying the active task in the OS X menu bar)

Here is a script to remove the friction of manually typing and deleting a @now or @next tag of this kind. It can be attached to a keystroke with something like Keyboard Maestro or FastScripts.

The script:

  1. Places a @now tag at the top of the outline if there is no such tag in the file
  2. If there is already a @now tag somewhere, it just moves it on to the next task that is not @done, unfolding part of the outline to make the newly tagged line visible, if necessary.

That’s it, but also:

If you edit the 1 (or true) argument in the last line of the script to 0 (or false), you can save it as a second script which moves the tag upwards rather than downwards in the outline:

  • to a preceding sibling if there is one,
  • or to the immediate parent.

and

If you are using TextBar to display the line marked with @now, you can also trying editing the 3rd argument to the main function (right at the end of the script) to 1 rather than 0.

This will force TextBar to update its display of your current task each time this script moves your @now or @next tag.

(It’s using a bit of an unsophisticated approach – if you are using TextBar, do write a quick note to its author to support the request for an Applescript .refresh() method which can trigger an immediate refresh a little more rapidly and elegantly : - )

SCRIPT:

// Move @now or @next tag to next available item in file
// Ver 0.9
// Requires TP3 Preview (build 166+)
// Skips @done items
// If necessary, unfolds to make newly tagged item visible
// If blnMarkDone=true:
//  1. Marks the current item as @done(yyyy-mm-dd HH:mm) 
//     before moving the @now/@next tag
//  2. If the parent project is now completed, marks that as @done too

(function (strNowTag, blnDown, blnTextBar, blnMarkDone) {
	// Adjust value for blnDown at foot of script:
	// 0/false: Move UP     (skipping @done items)
	// 1/true : Move DOWN   (skipping @done items)
	// blnTextBar: true:force refresh of TextBar, 
	//             false:not using Textbar, or don't force refresh
	'use strict';

	// Edit here to move a differently named bookmarker tag, 
	// like 'next' (--> @next)
	var strTag = strNowTag || 'now';

	var ds = Application("TaskPaper").documents,
		blnMoved = ds.length ? ds[0].evaluate({
			script: (
				// Evaluated in TP3 ********************
				function (editor, options) {

					// projects ancestral to this item
					// (plural because projects can nest)
					// tpItem -> [tpProject]
					function itemProjects(oItem) {
						return oItem.evaluateItemPath(
							'ancestor-or-self::@type=project'
						);
					}

					// descendant tasks not @done ?
					// tpItem -> [tpItem]
					function tasksRemaining(oItem) {
						return oItem.evaluateItemPath(
							'descendant::(@type=task and not @done)'
						);
					}

					// monadic bind/chain for lists
					// [a] -> (a -> [b]) -> [b]
					function chain(xs, f) {
						return [].concat.apply([], xs.map(f));
					}

					var strTag = options.tag,
						strAttrib = 'data-' + strTag,
						oOutline = editor.itemBuffer.outline,
						lstNow = oOutline.evaluateItemPath('//@' + strTag + '[0]');

					if (lstNow.length) {
						var oNow = lstNow[0],
							blnFound = false,
							blnDown = options.downward,
							lstNext = chain(
								blnDown ? [
                                    'following', 'ancestor'
                                ] : [
                                    'preceding', 'ancestor/descendant'
                        ],
								function (x) {
									if (blnFound) return [];
									else {
										var lst = oNow.evaluateItemPath(
												x + '::(@type=task and not @done)[' +
												(blnDown ? '0' : '-1') + ']'
											),
											lng = lst.length,
											oNext = lng ? [lst[0]] : [];

										blnFound = blnFound || lng;
										return oNext;
									}
								}
							),
							oNext = lstNext.length ? lstNext[0] : null;

						if (options.markDone) {
							if (oNow.getAttribute('data-type') === 'task') {
								var strTime = moment().format('YYYY-MM-DD HH:mm');

								oNow.setAttribute('data-done', strTime);

								// also mark any projects finished by this as done
								itemProjects(oNow).filter(function (p) {
									return tasksRemaining(p).length === 0;
								}).forEach(function (x) {
									x.setAttribute('data-done', strTime);
								});
							}
						}

						// clear any existing @now tags
						lstNow.forEach(function (x) {
							return x.removeAttribute(strAttrib);
						});
					}

					var oTaggable = (oNext && !oNext.isRoot) ? oNext : (
						function () {
							var lstDoable = oOutline.evaluateItemPath(
								'//(not @done)[' + (blnDown ? '0' : '-1') + ']'
							);
							return lstDoable.length ? lstDoable[0] : null;
						}()
					);

					return oTaggable ? (
						oTaggable.setAttribute(strAttrib, ''),
						editor.isVisible(
							oTaggable
						) || editor.makeVisible(oTaggable),
						true
					) : false;
				}

				//**************************************
			).toString(),
			withOptions: {
				downward: blnDown,
				tag: strTag,
				markDone: blnMarkDone
			}
		}) : false;

	// If you are using [TextBar](http://www.richsomerfield.com/apps/)
	// to display the active TaskPaper task on the OS X menu bar, as described in:
	// http://support.hogbaysoftware.com/t/script-displaying-the-active-task-in-the-os-x-menu-bar/1290

	// then uncommenting the following 4 lines 
	// will force an immediate update of TextBar

	//*****************************************
	// COMMENT OUT 12 LINES (TO NEXT LINE OF ASTERISKS) IF NOT USING TEXTBAR
	if (blnTextBar && blnMoved) {
		try {
			var a = Application('TextBar');
          ['quit', 'activate'].forEach(
				function (m) {
					a[m]();
				}
			)
		} catch (_) {
			return 'Textbar not running';
		}
	}
	//******************************************

	if (!blnMoved) {
		var a = Application.currentApplication();

		(a.includeStandardAdditions = true, a).displayNotification(
			'All done in active TP3 file !', {
				withTitle: "TaskPaper 3 active tasks",
				soundName: "Glass"
			}
		);
	};
	return blnMoved;

	// EDIT VALUE FOR blnDown (see top of script)
	// 0: Move tag up  (skipping @done items)
	// 1: Move tag down (skipping @done items)
	// strTagName, blnDown, blnTextBar, blnMarkDone (1: true, 0:false)
})('now', true, true, false);
2 Likes

sorry for being dense but what language is this? ty

As long as you are using Yosemite onwards, you can run either Applescript or Apple’s newer JavaScript for Applications in the OS X Script Editor (or in Keyboard Maestro, FastScripts etc).

If you want to run a JavaScript for Applications script like this, make sure that the drop-down at the top left of Script Editor is set to JavaScript.

The section of the script marked Evaluated in TP3 (between rows of asterisks) is evaluated by TaskPaper itself, rather than by Script Editor / osascript, but is still the same webkit/Safari dialect of JavaScript.

(You can write the outer part of a TP3 script in Applescript, but you would still have to pass some JavaScript (as a string) to the TP3 document.evaluate() function. (JavaScript is the native scripting language of TaskPaper 3).

(JavaScript is so widely used on the web and elsewhere, that learning even a little does quickly reap rewards, not only on the web and OS X. but also on iOS, where it is the scripting vehicle of things like iOS Drafts, and can be used with the iOS (and OS X) versions of TextExpander.

Keyboard Maestro has actions for executing JavaScript for Applications (as well, of course, as browser JavaScript),

and in a shell script, or in Terminal.app you can run JS for apps (or ‘JXA’) by wrapping it as follows:

osascript -l JavaScript <<JXA_END 2>/dev/null

    // Javascript for Applications code here ...

JXA_END
1 Like

Note that the first draft of the code above was for TP3 version 165

Rob

UPDATE

The code in the previous post is now updated for (and requires) TP3 Preview version 166.

(It has also been updated to:

  1. skip over items marked @done
  2. unfold the outline, where necessary, to make a newly tagged item visible )

hmm, all it is doing for me is moving the @now tag down to next task.

also does it ignore projects and notes, only tagging tasks?

ty

does it ignore projects and notes, only tagging tasks?

If you would like it to move only from task to task (skipping projects and notes) you can edit the line (middle of script):

'::(not @done)[' + (blnDown ? '0' : '-1') + ']'),

changing it to:

'::(@type=task and not @done)[' + (blnDown ? '0' : '-1') + ']'),

If you would like to force an update to the TaskBar display (in advance of the refresh interval), you can edit the last line of the script to change the final zero to a one. (i.e. giving blnTextBar a non zero value).

Is there any behaviour that you had in mind, or would like to see ?

(The goal of the script is simply to streamline moving from one task to another, without having to manually delete and move/type the bookmark tag. One option that could be simply added to it would be flagging a task as @done – possibly with a timestamp – before moving the @now tag on to the next task).

Updated (in original post) to version 6.

Adds a fourth parameter, blnMarkDone, to the end of the script.

If blnMarkDone is true or 1 then:

  1. The current item will be time-stamped as @done(yyyy-mm-dd hh:mm) before the @now or @next tag moves on.
  2. If completion of that task finishes off a whole project, that project will also be marked ‘@done’, with a time-stamp.

In another thread, I was asked about how some sections of this script work, so …

here is an alternative version, with one or two sections rewritten to break them down into stages, with a few more comments, and a few more named variables (to replace direct composition)

// USABLE ALTERNATIVE VERSION WITH A FEW SECTIONS BROKEN DOWN INTO
// COMMENTED STAGES

// Move @now or @next tag to next available item in file
// Ver 0.9 version B, for teaching
// Requires TP3 Preview (build 166+)
// Skips @done items
// If necessary, unfolds to make newly tagged item visible
// If blnMarkDone=true:
//  1. Marks the current item as @done(yyyy-mm-dd HH:mm) 
//     before moving the @now/@next tag
//  2. If the parent project is now completed, marks that as @done too

(function (strNowTag, blnDown, blnTextBar, blnMarkDone) {
    // Adjust value for blnDown at foot of script:
    // 0/false: Move UP     (skipping @done items)
    // 1/true : Move DOWN   (skipping @done items)
    // blnTextBar: true:force refresh of TextBar, 
    //             false:not using Textbar, or don't force refresh
    'use strict';

    // Edit here to move a differently named bookmarker tag, 
    // like 'next' (--> @next)
    var strTag = strNowTag || 'now';

    var ds = Application("TaskPaper").documents,
        blnMoved = ds.length ? ds[0].evaluate({
            script: (
                // Evaluated in TP3 ********************
                function (editor, options) {

                    // projects ancestral to this item
                    // (plural because projects can nest)
                    // tpItem -> [tpProject]
                    function itemProjects(oItem) {
                        return oItem.evaluateItemPath(
                            'ancestor-or-self::@type=project'
                        );
                    }

                    // descendant tasks not @done ?
                    // tpItem -> [tpItem]
                    function tasksRemaining(oItem) {
                        return oItem.evaluateItemPath(
                            'descendant::(@type=task and not @done)'
                        );
                    }

                    // monadic bind/chain for lists
                    // [a] -> (a -> [b]) -> [b]
                    function chain(xs, f) {
                        return [].concat.apply([], xs.map(f));
                    }

                    var strTag = options.tag,
                        strAttrib = 'data-' + strTag,
                        oOutline = editor.itemBuffer.outline,
                        lstNow = oOutline.evaluateItemPath('//@' + strTag + '[0]');

                    if (lstNow.length) {
                        var oNow = lstNow[0],
                            blnFound = false,
                            blnDown = options.downward,
                            lstNext = chain(
                                blnDown ? [
                                    'following', 'ancestor'
                                ] : [
                                    'preceding', 'ancestor/descendant'
                                ],
                                function (x) {
                                    if (blnFound) return [];
                                    else {
                                        var lst = oNow.evaluateItemPath(
                                                x + '::(@type=task and not @done)[' +
                                                (blnDown ? '0' : '-1') + ']'
                                            ),
                                            lng = lst.length,
                                            oNext = lng ? [lst[0]] : [];

                                        blnFound = blnFound || lng;
                                        return oNext;
                                    }
                                }
                            ),
                            oNext = lstNext.length ? lstNext[0] : null;

                        if (options.markDone) {
                            if (oNow.getAttribute('data-type') === 'task') {
                                var strTime = moment().format('YYYY-MM-DD HH:mm');

                                oNow.setAttribute('data-done', strTime);

                                // ORIGINAL CODE
                                //also mark any projects finished by this as done
                                // itemProjects(oNow).filter(function (p) {
                                //  return tasksRemaining(p).length === 0;
                                // }).forEach(function (x) {
                                //  x.setAttribute('data-done', strTime);
                                // });

                                // REWRITTEN INTO STAGES WITH NAMED VARIABLES
                                // ( rather than using direct composition, as above )

                                // STAGE ONE
                                var lstItemProjects = itemProjects(oNow),

                                    // STAGE TWO
                                    // Collect any projects that are now full completed
                                    lstFullyCompleted = lstItemProjects.filter(
                                        function (p) {
                                            return tasksRemaining(p).length === 0;
                                        }
                                    );

                                // STAGE THREE
                                // Mark each project that now has no remaining tasks as done
                                lstFullyCompleted.forEach(
                                    function (x) {
                                        x.setAttribute('data-done', strTime);
                                    }
                                );
                            }
                        }

                        // clear any existing @now tags
                        lstNow.forEach(function (x) {
                            return x.removeAttribute(strAttrib);
                        });
                    }

                    // ORIGINAL CODE
                    //                  var oTaggable = (oNext && !oNext.isRoot) ? oNext : (
                    //                      function () {
                    //                          var lstDoable = oOutline.evaluateItemPath(
                    //                              '//(not @done)[' + (blnDown ? '0' : '-1') + ']'
                    //                          );
                    //                          return lstDoable.length ? lstDoable[0] : null;
                    //                      }()
                    //                  );
                    // 
                    //                  return oTaggable ? (
                    //                      oTaggable.setAttribute(strAttrib, ''),
                    //                      editor.isVisible(
                    //                          oTaggable
                    //                      ) || editor.makeVisible(oTaggable),
                    //                      true
                    //                  ) : false;

                    //  BROKEN DOWN INTO STAGES WITH NAMED VARIABLES

                    // We now have a value for oNext (the next line in the sequence)
                    // and need to check whether it can be directly tagged, 
                    // or whether it has any uncompleted children which should be tagged first

                    var oTaggable;
                    
                    if (!oNext || oNext.isRoot) {
                        // if oNext is null, or is the virtual/invisible 'root' of the outline, 
                        // then it can't be tagged.
                        // We need to find the next non-completed line where 'next' means nearest top of doc if blnDown
                        // or nearest end of doc if blnUp
                        var strQuery,
                            lstDoable;
                        
                        if (blnDown) {
                            strQuery = '//(not @done)[0]'; // first match
                        } else {
                            strQuery = '//(not @done)[-1]'; // last match
                        }
                        
                        lstDoable = oOutline.evaluateItemPath(strQuery);
                        // if we have a match then use it (the list will only be one item long)
                        if (lstDoable.length) {  // non-zero evaluates to boolean true
                            var oTaggable = lstDoable[0];
                        }
                    } else {
                        // We can use oNext
                        oTaggable = oNext;
                    }
                    
                    if (oTaggable) {
                        // We have found something. Let's tag it
                        oTaggable.setAttribute(strAttrib, ''); // its not @now(value) just @now('') = @now
                        
                        // Let's also make sure that it's not hidden by outline folding/filtering)
                        if (!editor.isVisible(oTaggable)) {
                            editor.makeVisible(oTaggable);
                        }
                        
                        return true; // done
                    } else {
                        return false; // there was nothing to tag
                    }
                }


                //**************************************
            ).toString(),
            withOptions: {
                downward: blnDown,
                tag: strTag,
                markDone: blnMarkDone
            }
        }) : false;

    // If you are using [TextBar](http://www.richsomerfield.com/apps/)
    // to display the active TaskPaper task on the OS X menu bar, as described in:
    // http://support.hogbaysoftware.com/t/script-displaying-the-active-task-in-the-os-x-menu-bar/1290

    // then uncommenting the following 4 lines 
    // will force an immediate update of TextBar

    //*****************************************
    // COMMENT OUT 12 LINES (TO NEXT LINE OF ASTERISKS) IF NOT USING TEXTBAR
    if (blnTextBar && blnMoved) {
        try {
            var a = Application('TextBar');
          ['quit', 'activate'].forEach(
                function (m) {
                    a[m]();
                }
            )
        } catch (_) {
            return 'Textbar not running';
        }
    }
    //******************************************

    if (!blnMoved) {
        var a = Application.currentApplication();

        (a.includeStandardAdditions = true, a).displayNotification(
            'All done in active TP3 file !', {
                withTitle: "TaskPaper 3 active tasks",
                soundName: "Glass"
            }
        );
    };
    return blnMoved;

    // EDIT VALUE FOR blnDown (see top of script)
    // 0: Move tag up  (skipping @done items)
    // 1: Move tag down (skipping @done items)
    // strTagName, blnDown, blnTextBar, blnMarkDone (1: true, 0:false)
})('now', true, false, true);

This script was written during the TaskPaper 3 Beta, before the final scripting interface was adopted.

Here is an update in two parts:

  1. The Javascript for Automation source
  2. An updated version of the .sh file for TextBar

In TextBar > Preferences > Advanced, you will need to make sure that the text field is clear (perhaps removing the default -l switch, which results in a display of shell variables, displacing the script output)

Javascript for Automation script

// Move @now or @next tag to next available item in file
// Ver 0.10
// Requires TaskPaper 3
// Skips @done items
// If necessary, unfolds to make newly tagged item visible
// If blnMarkDone=true:
//  1. Marks the current item as @done(yyyy-mm-dd HH:mm)
//     before moving the @now/@next tag
//  2. If the parent project is now completed, marks that as @done too

(function (strNowTag, blnDown, blnTextBar, blnMarkDone) {
    // Adjust value for blnDown at foot of script:
    // 0/false: Move UP     (skipping @done items)
    // 1/true : Move DOWN   (skipping @done items)
    // blnTextBar: true:force refresh of TextBar,
    //             false:not using Textbar, or don't force refresh
    'use strict';

    // Edit here to move a differently named bookmarker tag,
    // like 'next' (--> @next)
    var strTag = strNowTag || 'now';

    var tp = Application('TaskPaper'),
        ds = tp.documents;
    var //blnMoved = ds.length ? ds.byName('test.taskpaper').evaluate({
        // default is to watch front document: ds.at(0)
        blnMoved = ds.length ? ds.at(0).evaluate({
        script: (
                // Evaluated in TP3 ********************
                function (editor, options) {

                    var outline = editor.outline;

                    // projects ancestral to this item
                    // (plural because projects can nest)
                    // tpItem -> [tpProject]
                    function itemProjects(oItem) {
                        return outline.evaluateItemPath(
                            'ancestor-or-self::@type=project',
                            oItem
                        );
                    }

                    // descendant tasks not @done ?
                    // tpItem -> [tpItem]
                    function tasksRemaining(oItem) {
                        return outline.evaluateItemPath(
                            'descendant::(@type=task and not @done)',
                            oItem
                        );
                    }

                    // monadic bind/chain for lists
                    // [a] -> (a -> [b]) -> [b]
                    function chain(xs, f) {
                        return [].concat.apply([], xs.map(f));
                    }

                    var strTag = options.tag,
                        strAttrib = 'data-' + strTag,
                        lstNow = outline.evaluateItemPath('//@' + strTag + '[0]');

                    if (lstNow.length) {
                        var oNow = lstNow[0],
                            blnFound = false,
                            blnDown = options.downward,
                            lstNext = chain(
                                blnDown ? [
                                    'following', 'ancestor'
                                ] : [
                                    'preceding', 'ancestor/descendant'
                                ],
                                function (x) {
                                    if (blnFound) return [];
                                    else {
                                        var lst = outline.evaluateItemPath(
                                                x + '::(@type=task and not @done)[' +
                                                (blnDown ? '0' : '-1') + ']',
                                                oNow
                                            ),
                                            lng = lst.length,
                                            oNext = lng ? [lst[0]] : [];

                                        blnFound = blnFound || lng;
                                        return oNext;
                                    }
                                }
                            ),
                            oNext = lstNext.length ? lstNext[0] : null;

                        if (options.markDone) {
                            if (oNow.getAttribute('data-type') === 'task') {
                                var strTime = moment()
                                    .format('YYYY-MM-DD HH:mm');

                                oNow.setAttribute('data-done', strTime);

                                // also mark any projects finished by this as done
                                itemProjects(oNow)
                                    .filter(function (p) {
                                        return tasksRemaining(p)
                                            .length === 0;
                                    })
                                    .forEach(function (x) {
                                        x.setAttribute('data-done', strTime);
                                    });
                            }
                        }

                        // clear any existing @now tags
                        lstNow.forEach(function (x) {
                            return x.removeAttribute(strAttrib);
                        });
                    }

                    var oTaggable = (oNext && !oNext.isRoot) ? oNext : (
                        function () {
                            var lstDoable = outline.evaluateItemPath(
                                '//(not @done)[' + (blnDown ? '0' : '-1') + ']'
                            );
                            return lstDoable.length ? lstDoable[0] : null;
                        }()
                    );

                    return oTaggable ? (
                        oTaggable.setAttribute(strAttrib, ''),
                        editor.isDisplayed(
                            oTaggable
                        ) || editor.forceDisplayed(oTaggable),
                        true
                    ) : false;
                }

                //**************************************
            )
            .toString(),
        withOptions: {
            downward: blnDown,
            tag: strTag,
            markDone: blnMarkDone
        }
    }) : false;

    // If you are using [TextBar](http://www.richsomerfield.com/apps/)
    // to display the active TaskPaper task on the OS X menu bar, as described in:
    // http://support.hogbaysoftware.com/t/script-displaying-the-active-task-in-the-os-x-menu-bar/1290

    // then uncommenting the following 4 lines
    // will force an immediate update of TextBar

    //*****************************************
    // COMMENT OUT 12 LINES (TO NEXT LINE OF ASTERISKS) IF NOT USING TEXTBAR
    if (blnTextBar && blnMoved) {
        try {
            var a = Application('TextBar');
            ['quit', 'activate'].forEach(
                function (m) {
                    a[m]();
                }
            )
        } catch (_) {
            return 'Textbar not running';
        }
    }
    //******************************************

    if (!blnMoved) {
        var a = Application.currentApplication();

        (a.includeStandardAdditions = true, a)
        .displayNotification(
            'All done in active TP3 file !', {
                withTitle: "TaskPaper 3 active tasks",
                soundName: "Glass"
            }
        );
    };

    return blnMoved;

    // EDIT VALUE FOR blnDown (see top of script)
    // 0: Move tag up  (skipping @done items)
    // 1: Move tag down (skipping @done items)
    // strTagName, blnDown, blnTextBar, blnMarkDone (1: true, 0:false)
})('now', true, true, true);

Shell script for TextBar > Preferences (paste into Script field)

osascript -l JavaScript <<JXA_END 2>/dev/null
((intMaxParts) => {
    'use strict';
    // ver 0.4 Requires TaskPaper 3
    // (FIRST LINE TAGGED WITH @NOW and optionally a path to it)
    // Increase the value of intMaxParts (argument at end of script)
    // to lengthen the path displayed

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // bindEither (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindEither = (m, mf) =>
        m.Right !== undefined ? (
            mf(m.Right)
        ) : m;

    // concatMap :: (a -> [b]) -> [a] -> [b]
    const concatMap = (f, xs) =>
        xs.length > 0 ? [].concat.apply([], xs.map(f)) : [];

    const intMax = intMaxParts || 99,
        strSteps = concatMap(
            x => x ? [x.replace(/^- /, '')] : [],
            (() => {
                const
                    ds = Application("TaskPaper")
                    .documents;
                return bindEither(
                    ds.length > 0 ? Right(ds[0]) : Left(''),
                    d => Right(d.evaluate({
                        script: ((editor, options) => {
                                // htmlEncoded :: String -> String
                                const htmlEncoded = s => {
                                    const rgx = /[\w\s]/;
                                    return ''.concat.apply('',
                                        s.split('')
                                        .map(c => rgx.test(c) ? (
                                            c
                                        ) : '&#' + c.charCodeAt(0) + ';')
                                    );
                                };
                                return editor.outline.evaluateItemPath(
                                        options.query
                                    )
                                    .map(x => htmlEncoded(
                                        x.bodyContentString
                                    ))
                            })
                            .toString(),
                        withOptions: {
                            query: '//@now[0]/ancestor-or-self::*[-' +
                                intMax + ':]'
                        }
                    }))
                    .Right
                );
            })()
        )
        .join(' <span style="color:#8B4513">-&gt;</span> '); // Arrow

    return strSteps ? '<html><font style=color:#FFA361; font-family:"Menlo">' +
        strSteps + '</font></html>' : '❓';
})(3); // MAX NUMBER OF PATH SEGMENTS TO INCLUDE
JXA_END


1 Like

An update - the version that I am currently using. (Requires TaskPaper 3 and macOS Sierra onwards)

Again, this script just:

  • moves a @now tag on the the next available item
  • optionally flags the previous @now item as @done(timestamped)
  • optionally updates a TextBar display of the current @now item and its context (parent, and any grandparent)
  • Optionally issues a macOS notification and sound. (One sound for a new available item, another sound for a completed outline)

Next available item means, for example, the next outline leaf (childless item) which is not @done

The movement through the outline is ‘bottom up’, or ‘preorder’ (children before parents). The @now tag moves to parent items only after all of their descendants are @done (so that the parent itself can be marked @done, if that is what is wanted)

( Items of all types are visited in the same way – projects, tasks and notes – I seem to be working these days with very few bullets and project colons – mostly just unadorned outlines )

This is a JavaScript for Automation script, for installation and use see:

// Move @now or @next tag to next available item in file

// Ver 0.16
// Requires TaskPaper 3, macOS Sierra onwards

// Skips @done items
// If necessary, unfolds to make newly tagged item visible
// If the option markAsDone = true: (see foot of script )
//  1. Marks the current item as @done(yyyy-mm-dd HH:mm)
//     before moving the @now/@next tag
//  2. If the parent project is now completed, marks that as @done too

// Author: Rob Trew (c) 2018 License: MIT

(options => {
    // OPTIONS (see foot of script)
    //   tagName : 'now',
    //   markAsDone : true, // when tag moves on
    //   useTextBar : true, // [TextBar](http://www.richsomerfield.com/apps/)
    //   notify : true // mac OS notification and sound on tag move
    'use strict';

    ObjC.import('AppKit');

    // TASKPAPER CONTEXT -----------------------------------------------------

    // taskPaperContext :: TP Editor -> Dict -> a
    const taskPaperContext = (editor, options) => {

        // GENERICS FOR TASKPAPER CONTEXT -------------------------------------

        // Left :: a -> Either a b
        const Left = x => ({
            type: 'Either',
            Left: x
        });

        // Right :: b -> Either a b
        const Right = x => ({
            type: 'Either',
            Right: x
        });

        // append (++) :: [a] -> [a] -> [a]
        // append (++) :: String -> String -> String
        const append = (xs, ys) => xs.concat(ys);

        // bindEither (>>=) :: Either a -> (a -> Either b) -> Either b
        const bindEither = (m, mf) =>
            m.Right !== undefined ? (
                mf(m.Right)
            ) : m;

        // concatMap :: (a -> [b]) -> [a] -> [b]
        const concatMap = (f, xs) =>
            xs.length > 0 ? [].concat.apply([], xs.map(f)) : [];

        // dropWhile :: (a -> Bool) -> [a] -> [a]
        const dropWhile = (p, xs) => {
            let i = 0;
            for (let lng = xs.length;
                (i < lng) && p(xs[i]); i++) {}
            return xs.slice(i);
        };

        // either :: (a -> c) -> (b -> c) -> Either a b -> c
        const either = (lf, rf, e) =>
            isLeft(e) ? (
                lf(e.Left)
            ) : isRight(e) ? (
                rf(e.Right)
            ) : undefined;

        // filter :: (a -> Bool) -> [a] -> [a]
        const filter = (f, xs) => xs.filter(f);

        // isLeft :: Either a b -> Bool
        const isLeft = lr =>
            lr.type === 'Either' && lr.Left !== undefined;

        // isRight :: Either a b -> Bool
        const isRight = lr =>
            lr.type === 'Either' && lr.Right !== undefined;

        // tail :: [a] -> [a]
        const tail = xs => xs.length > 0 ? xs.slice(1) : [];

        // TP ----------------------------------------------------------------
        const outline = editor.outline;

        // Bottom up traversal, ending with the supplied root item itself
        // postOrderTraversal :: TP3Item -> [TP3Item]
        const postOrderTraversal = item =>
            item.hasChildren ? (
                append(
                    concatMap(
                        postOrderTraversal,
                        filter(
                            x => x.bodyContentString.length > 0,
                            item.children
                        )
                    ), [item]
                )
            ) : [item];

        // topAncestor :: TP3Item -> TP3Item
        const topAncestor = item =>
            outline.evaluateItemPath(
                'ancestor-or-self::(@id!=Birch)[0]',
                item
            )[0];

        // First leaf descendant without @done tag
        // greenLeafEither :: TP3Item -> Either String TP3Item
        const greenLeafEither = item => {
            const
                xs = outline.evaluateItemPath(
                    'descendant::((count(.*)=0) and (not @done))',
                    item
                );
            return xs.length > 0 ? (
                Right(xs[0])
            ) : Left('Outline @done');
        };

        // nextItemEither :: TP3Item -> Either String TP3Item
        const nextItemEither = item =>
            either(
                _ => {
                    const
                        strID = item.id,
                        xs = filter(
                            x =>
                            !x.hasAttribute('data-done') &&
                            x.bodyContentString.length > 0,
                            tail(
                                dropWhile(
                                    x => strID !== x.id,
                                    postOrderTraversal(
                                        topAncestor(item)
                                    )
                                )
                            )
                        );
                    return xs.length > 0 ? (
                        Right(xs[0])
                    ) : Left('Finished');
                },
                Right,
                greenLeafEither(item)
            );

        // MAIN --------------------------------------------------------------
        const
            strTag = options.tagName || 'now',
            strAttrib = 'data-' + strTag,
            lstTagged = outline.evaluateItemPath('//@' + strTag),
            lrTagged = lstTagged.length > 0 ? (
                Right(lstTagged[0])
            ) : Left('No @' + strTag + ' tag found.');

        // EFFECTS ON ANY ITEM THAT HAD THE TAG  -----------------------------
        return ( // result :: Either String String
            isRight(lrTagged) && (
                x => (
                    x.removeAttribute(strAttrib),
                    options.markAsDone && x.setAttribute(
                        'data-done', moment()
                        .format('YYYY-MM-DD HH:mm')
                    )
                )
            )(lrTagged.Right),

            // EFFECTS ON ANY ITEM THAT NOW GETS THE TAG ---------------------
            bindEither(
                either(
                    _ => greenLeafEither(
                        editor.selection.startItem
                    ),
                    nextItemEither,
                    lrTagged
                ),
                item => (
                    // Effect ------------------------------------------------
                    editor.forceDisplayed(item, true),
                    item.setAttribute(strAttrib, ''),

                    // Value -------------------------------------------------
                    Right(
                        item.parent.bodyContentString +
                        ' ⟶ ' + item.bodyContentString
                    )
                )
            )
        );
    };

    // GENERICS FOR JXA CONTEXT  ---------------------------------------------

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // bindEither (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindEither = (m, mf) =>
        m.Right !== undefined ? (
            mf(m.Right)
        ) : m;

    // Simpler 2 argument only version of curry
    // curry :: ((a, b) -> c) -> a -> b -> c
    const curry = f => a => b => f(a, b);

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = (lf, rf, e) =>
        e.type === 'Either' ? (
            e.Left !== undefined ? (
                lf(e.Left)
            ) : rf(e.Right)
        ) : undefined;

    // isLeft :: Either a b -> Bool
    const isLeft = lr =>
        lr.type === 'Either' && lr.Left !== undefined;

    // isRight :: Either a b -> Bool
    const isRight = lr =>
        lr.type === 'Either' && lr.Right !== undefined;


    // JXA FUNCTIONS ---------------------------------------------------------

    // appIsInstalled :: String -> Bool
    const appIsInstalled = strBundleId =>
        ObjC.unwrap(
            $.NSWorkspace.sharedWorkspace
            .URLForApplicationWithBundleIdentifier(
                strBundleId
            )
            .fileSystemRepresentation
        ) !== undefined;

    // standardAdditions :: () -> Library Object
    const standardAdditions = () =>
        Object.assign(
            Application.currentApplication(), {
                includeStandardAdditions: true
            }
        );

    // focusChange :: String -> String -> IO ()
    const focusChange = curry((strSoundName, strTitle) =>
        standardAdditions()
        .displayNotification(
            'TaskPaper notes', {
                withTitle: strTitle,
                subtitle: '@' + options.tagName,
                soundName: strSoundName
            }
        ));

    // MAIN -----------------------------------------------------------------
    const
        ds = Application('TaskPaper')
        .documents,
        lrMoved = bindEither(
            ds.length > 0 ? (
                Right(ds.at(0))
            ) : Left('No TaskPaper documents open'),
            d => d.evaluate({
                script: taskPaperContext.toString(),
                withOptions: options
            })
        );

    // If you are using [TextBar](http://www.richsomerfield.com/apps/)
    // to display the active TaskPaper task on the OS X menu bar, as described in:

    // http://support.hogbaysoftware.com/t/script-displaying-the-active-task-in-the-os-x-menu-bar/1290

    // then uncommenting the following 4 lines
    // will trigger an immediate refresh of the TextBar display.

    // -----------------------------------------------------------------------
    // COMMENT OUT 12 LINES (TO NEXT LINE OF DASHES) IF NOT USING TEXTBAR

    if (
        options.useTextBar &&
        appIsInstalled('com.RichSomerfield.TextBar')
    ) {
        try {
            Application('TextBar')
                .refreshall()
        } catch (_) {
            console.log('TextBar not running')
        }
    }

    // -----------------------------------------------------------------------

    // NOTIFICATION ----------------------------------------------------------
    options.notify && either(
        focusChange('Glass'),
        focusChange('Pop'),
        lrMoved
    );

    // VALUE RETURNED --------------------------------------------------------
    return lrMoved;

    // OPTIONS - EDIT BELOW --------------------------------------------------
})({
    tagName: 'now',
    markAsDone: true, // add @done(dateTime) when tag moves on
    useTextBar: true, // [TextBar](http://www.richsomerfield.com/apps/)
    notify: true // mac OS notification and sound on tag move
});

For a Keyboard Maestro version, and a sample TextBar shell script, see:

3 Likes