TaskPaper: move @now tag to next available item (marking previous as @done)

An update, after some years, of a Keyboard Maestro macro for TaskPaper which I thought I had posted here but can't immediately track down:

14

The macro (requires TaskPaper 3, macOS Sierra onwards) just:

  • moves a @now tag on the the next available TaskPaper 3 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 in TaskPaper these days with very few bullets and project colons – mostly just unadorned outlines )

TaskPaper move NOW mark DONE.kmmacros (29.4 KB)

image

For reference, the shell script which I am personally using with TextBar is:

osascript -l JavaScript <<JXA_END 2>/dev/null
((intMaxParts) => {
    'use strict';
    // ver 0.5 Requires TaskPaper 3
    // NB This custom version makes particular assumptions about
    // folders, filenames, and which file we want to query
    // Clue (not necessarily TP3's front document)
    // (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

    // EDIT AS REQUIRED:
    // NB See also strToday and dayDocName at start of MAIN below
    const notePath = '~/Notes';

    // 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)) : [];

    // filePath :: String -> FilePath
    const filePath = s =>
        ObjC.unwrap(ObjC.wrap(s)
            .stringByStandardizingPath);

    // isoLocal :: Date -> String
    const isoLocal = dte => {
        const xs = ['FullYear', 'Month', 'Date',
                'Hours', 'Minutes' //, 'Seconds', 'Milliseconds'
            ]
            .map((k, i) => {
                const s = (dte['get' + k]() + (i === 1 ? 1 : 0))
                    .toString();
                return (s.length === 1 ? '0' : '') + s;
            });
        return xs.slice(0, 3)
            .join('-') + ((xs[3] !== '00' || xs[4] !== '00') ? (
                ' ' + xs.slice(3)
                .join(':')
            ) : '');
    };

    // take :: Int -> [a] -> [a]
    const take = (n, xs) => xs.slice(0, n);

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

    // MAIN -----------------------------------------------------------------
    const
        strToday = take(10, isoLocal(new Date())),
        dayDocName = 'notes' + strToday + '.taskpaper',
        fpDayNotes = filePath(notePath) + '/' + dayDocName,
        intMax = intMaxParts || 99,
        strSteps = concatMap(
            x => x ? [x.replace(/^- /, '')] : [],
            (() => {
                const
                    tp3 = Application("TaskPaper"),
                    ds = tp3
                    .documents.where({
                        name: dayDocName
                    }),
                    docs = ds.length > 0 ? (
                        ds
                    ) : [(() => (
                        standardAdditions()
                        .doShellScript(
                            'touch "' + fpDayNotes + '"; ' +
                            'open -a TaskPaper "' + fpDayNotes + '";' +
                            'sleep 0.75'
                        ),
                        tp3
                        .documents.at(0)
                    ))()];
                return bindEither(
                    docs.length > 0 ? Right(docs[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
// 8B4513
    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