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: