TaskPaper 3 Quick Entry – send text to Inbox of a TaskPaper 3 file

taskpaper
jxa

#1

A first draft of a macro for sending text on the fly to the Inbox project of a specific TaskPaper 3 file

Quick Entry for TaskPaper 3 .kmmacros (32.6 KB)

Source of the Execute JavaScript for Automation action:

// SEND TEXT LINES TO INBOX: PROJECT OF FRONT TASKPAPER DOCUMENT

// Draft 0.06
// Adds experimental options to:
//      - add items to top of Inbox, rather than end
//      - append an @added(yyy-mm-dd HH:MM) task to top level items
//      - parse any informal date content of tags in the line

// ( Set options at end of script )

// This version just gets its text from the clipboard
// Could be adapted for Keyboard Maestro, LaunchBar, Alfred etc

// var a = Application.currentApplication(),
//     sa = (a.includeStandardAdditions = true, a);

var kmVars = Application("Keyboard Maestro Engine")
    .variables;



// N.B. you need to give a filepath to an existing TaskPaper document
// in the:
// OPTIONS SETTINGS AT BOTTOM OF SCRIPT


(function (dctOptions) {
    'use strict';

    // TASKPAPER CONTEXT ***************************************

    function TaskPaperContext(editor, options) {

        // FIND OR MAKE A PROJECT TO HOLD INCOMING TEXT
        // String -> String -> {project: tpItem, new: Bool}
        function projectAtPath(strProjectPath, strDefaultName) {
            var strDefault = strDefaultName || 'Inbox:',
                outline = editor.outline,
                lstMatch = outline.evaluateItemPath(strProjectPath),
                blnFound = lstMatch.length > 0;

            return {
                project: blnFound ? lstMatch[0] : (function () {
                    var defaultProject = outline.createItem(
                        strDefault)

                    outline.groupUndoAndChanges(function () {
                        outline.root.appendChildren(
                            defaultProject
                        );
                    });

                    return defaultProject;
                })(),
                new: !blnFound
            };
        }

        // parsedLines :: String -> [{indent: Int, text:String, bulleted:Bool}]
        function parsedLines(strText) {

            // tagDatesAsISO :: String -> String
            function tagDatesAsISO(s) {

                function maybeTrans(str) {
                    var strParse = dt.format(dt.parse(str.slice(1, -1)));

                    return strParse === "Invalid date" ? str : (
                        '(' + strParse.substr(0, 16) + ')'
                    );
                }

                return s.split(rgxTag)
                    .map(function (x, i) {
                        return ((i + 1) % 3) === 0 ? maybeTrans(x) : x;
                    })
                    .join('');
            }

            var rgxLine = /^(\s*)([\-\*\+]*)(\s*)/,
                rgxTag = /(\s+\@\w+)(\([^\)]+\))/g,
                dt = DateTime;

            return strText.split(/[\n\r]+/)
                .map(function (x) {
                    var ms = rgxLine.exec(x),
                        lngIndent = (ms ? ms[1] : '')
                        .replace(/    /g, '\t')
                        .length,
                        strRaw = (x.slice(
                            (ms ? ms[0] : '')
                            .length
                        )),
                        strText = blnDates ? tagDatesAsISO(strRaw) :
                        strRaw;

                    return {
                        indent: lngIndent,
                        text: strText + (
                            (blnTimeStamp && (lngIndent < 1)) ?
                            strNow :
                            ''
                        ),
                        bulleted: ms[2].length > 0
                    };
                });
        }

        // textNest :: [{indent:Int, text:String}, bulleted:Bool]
        //          -> Tree {text:String, nest:[Tree]}
        function textNest(xs) {
            var h = xs.length > 0 ? xs[0] : undefined,
                lstLevels = [{
                    text: undefined,
                    nest: []
            }];

            if (h) {
                var lngBase = h.indent;

                xs.forEach(function (x) {
                    var lngMax = lstLevels.length - 1,
                        lngLevel = x.indent - lngBase,
                        dctParent = lngLevel > lngMax ? lstLevels[
                            lngMax] : lstLevels[lngLevel],
                        dctNew = {
                            bullet: lngLevel === 0 || x.bulleted,
                            text: x.text,
                            nest: []
                        };

                    dctParent.nest.push(dctNew);

                    if (lngLevel > lngMax) lstLevels.push(dctNew);
                    else lstLevels[lngLevel + 1] = dctNew;
                });

            }
            return lstLevels[0];
        }

        // insertNest :: tp3Node ->
        //      Tree {text:String, nest:[Tree], bullet:Bool} -> ()
        function insertNest(oParent, oTextNest, blnAtTop) {

            // placeSubNest :: tp3Node -> Tree {text:String, nest:[Tree], bullet:Bool} -> ()
            function placeSubNest(oParent, lstNest, blnTop) {
                // IMMEDATE CHILD NODES CREATED, 
                if (lstNest.length > 0) {
                    var lstChiln = lstNest.map(function (dct) {
                        return outline.createItem((dct.bullet ?
                                '- ' : '') +
                            (dct.text || ''));
                    });

                    // AND PLACED UNDER EXISTING PARENT LINE
                    outline.groupUndoAndChanges(function () {
                        oParent[
                            blnTop ? 'insertChildrenBefore' :
                            'appendChildren'
                            ](lstChiln, oParent.firstChild);
                    });

                    // THEN RECURSION WITH EACH CHILD FOR 
                    // ITS DESCENDANTS, IF ANY
                    zipWith(function (dctNest, oNode) {
                        var lstSub = dctNest.nest;

                        if (lstSub.length > 0) {
                            placeSubNest(oNode, lstSub, false);
                        }
                    }, lstNest, lstChiln);
                }
            }

            // Ensure that the nest has a virtual root
            var outline = editor.outline;

            // Place the nest beneath the parent
            placeSubNest(oParent, oTextNest.nest, blnAtTop)
        }

        // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
        function zipWith(f, xs, ys) {
            var ny = ys.length;
            return (xs.length <= ny ? xs : xs.slice(0, ny))
                .map(function (x, i) {
                    return f(x, ys[i]);
                });
        }


        // TASKPAPER CONTEXT MAIN:

        // 1. FIND OR CREATE AN INBOX
        var outline = editor.outline,
            mInbox = projectAtPath(options.inboxPath, 'Inbox:'),
            itemInbox = mInbox.project;


        // 2. PARSE THE INCOMING TEXT TO A NEST
        var blnTimeStamp = options.withTimeStamp,
            strAdded = blnTimeStamp ? options.timeStampTag : '',
            strNow = blnTimeStamp ? (' @' + strAdded +
                '(' + DateTime.format(new Date())
                .substr(0, 16) + ')') : '',
            blnDates = options.parseTagDates,
            dctNest = textNest(
                parsedLines(
                    options.textLines
                )
            );

        // 3. INSERT THE TEXT NEST IN THE INBOX
        insertNest(
            itemInbox,
            dctNest,
            options.atTopOfList
        );

        return options.textLines
    }


    // JAVASCRIPT FOR AUTOMATION  CONTEXT ***********************

    // fileExists :: String -> Bool
    function fileExists(strPath) {
        var error = $();

        $.NSFileManager.defaultManager
            .attributesOfItemAtPathError(
                ObjC.unwrap($(strPath)
                    .stringByExpandingTildeInPath),
                error
            );
            
        return (error.code === undefined);
    }


    //  JSA MAIN ***********************************************


    var a = Application.currentApplication(),
        sa = (a.includeStandardAdditions = true, a);


    //1. DOES THE FILE EXIST ?

    var strFullPath = ObjC.unwrap(
        $(dctOptions.filePath)
        .stringByExpandingTildeInPath
    );

    if (fileExists(strFullPath)) {
        var tp3 = Application("TaskPaper"),
            d = tp3.open(Path(strFullPath));

        if (d) {
            var varResult = d.evaluate({
                script: TaskPaperContext.toString(),
                withOptions: dctOptions
            });
            
            if (dctOptions.saveAfterAdding) d.save();
            
            return varResult;
        }

    } else {
        sa.activate();
        sa.displayDialog('File not found:\n\n' + strFullPath, {
            //defaultAnswer: undefined,
            //buttons : undefined,
            defaultButton: 'OK',
            //cancelButton : undefined,
            withTitle: "Quick entry",
            withIcon: sa.pathToResource('TaskPaperAppIcon.icns', {
                inBundle: 'Applications/TaskPaper.app'
            }), // 'note'
            givingUpAfter: 30
        })
    }

    // SCRIPT OPTIONS

})({
    inboxPath: kmVars.TP3QEItemPath.value(), //'//Inbox and @type=project[-1]',
    filePath: kmVars.TP3QEFilePath.value(), //'~/Notes/general.taskpaper',
    
    // items to append to top (rather than end) of Inbox ?
    atTopOfList: kmVars['To top of list'].value() === '1', 
    
    // convert date-time tag contents to yyyy-mm-dd HH:MM ?
    parseTagDates: kmVars['Parsing tag dates'].value() === '1',
    
    // Appending a tag like @added(2016-03-24 20:05)
    withTimeStamp: kmVars['With time stamp'].value() === '1',
    
    timeStampTag: 'added', // specify a tag name like 'added' any time-stamping
    textLines: kmVars['Inbox note'].value(),
    
    saveAfterAdding: false
});

#2

Added a few options to the macro (above)

( Main one is the choice between sending new material to the start or end of the Inbox project )


#3

Minor tweak (updated above) - mainly to make the checkboxes remember their settings.

(Thanks to JMichaelTX for showing me how to do that)


#4

Added an option (at the foot of the main script)

     saveAfterAdding = true | false

Edit the value to true to make the TaskPaper file save automatically after a quick entry is made.


#5

Great macro (as always); thanks for that @ComplexPoint!
I was just wondering: is there any special syntax to be used in the “Inbox note” field to enable the addition of a note to the task or does the KM panel/JS Script need to be modified?


#6

Could certainly do that – any thoughts ?

Additional field (first renamed as task or item, additional one as ‘note’) ?
Or some syntactic convention ? (I forget what the big-bucket databases like OmniFocus do …)


#7

I still have a few old KM macros for TP2 that I built using a separate field in the KM input panel so my preference goes to that solution because I got used to it. Maybe it is also more clear than a dedicated syntax.

Providing the possibility to add a note to a task poses the issue of multiple tasks entered in the “Inbox note” field (now every new line is interpreted as a separate task). A dedicated syntax could allow the user to decide where to add a note while a “Task note” field will leave open the question to decide which task will receive the note.

Personally I always add one task at a time so, again, no problem with a “Task note” field.


#8

Before I add a note field (and thinking about the multiple entry issue which you mention) I have realised, looking at the code, that there is a (syntactic) route to adding notes in the existing macro:

Type the item as normal, and end it, beginning an attached note, by holding down ⌥ and tapping Return, Tab

⌥Return, ⌥Tab

Does that seem workable ?


#9

I tried that before posting but it did’t work. Now I realise that it’s due to the fact that I assigned ⌥Tab to Witch app.
When I disable Witch the syntax correctly produce a note.
So I think that your solution is fine if I change Witch shortcut to a different combination.

Thanks for taking the time to look into it.


#10

Does this macro still work? After I upgraded my OS to Sierra, I can’t seem to add entries to the files.
[MODIFIED] Now it’s working. I’m not sure what I changed. Thank you.
[READD] It doesn’t work now and then… I don’t know why this happens… Nothing is added to my file.