What's the latest on "Quick Entry" solutions for TaskPaper 3?


#1

Returning to TaskPaper after some time away, I’m wondering what folks are using as a substitute for the “Quick Entry” dialog no longer available in TP3?

I’m currently using LaunchBar’s “Prepend text file…” feature but I’d love to find something even easier. Anything using LaunchBar or KeyboardMaestro would be fine, but I’d like to have new entries automatically include the "- " prefix and end up in an “Inbox:” project in a pre-determined .taskpaper file.


What's missing in TaskPaper 3 Preview?
#2

Making a start on that here – this version just takes its text from the clipboard, and puts it in the Inbox: project of the specified TaskPaper 3 file.

(It’s not tested yet, so probably better to wait a bit : - )

(Over the weekend I’ll look at adapting it for Keyboard Maestro and LaunchBar)

(If you want to experiment with it, you will need to edit the filepath in the options at the bottom of this script)

Written in JavaScript for Automation:

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

// Predraft version 0.02
// 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); 

// 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) {
            var rgxLine = /^(\s*)([\-\*\+]*)(\s*)/;

            return strText.split(/[\n\r]+/)
                .map(function (x) {
                    var ms = rgxLine.exec(x);

                    return {
                        indent: (ms ? ms[1] : '')
                            .replace(/    /g, '\t')
                            .length,
                        text: x.slice((ms ? ms[0] : '')
                            .length),
                        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) {

            // placeSubNest :: tp3Node -> Tree {text:String, nest:[Tree], bullet:Bool} -> ()
            function placeSubNest(oParent, lstNest) {
                // 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.appendChildren(lstChiln);
                    });

                    // 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);
                        }
                    }, lstNest, lstChiln);
                }
            }

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

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

        // 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 dctNest = textNest(
            parsedLines(
                options.textLines
            )
        );

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

    }


    // 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));

        //2. And an inbox in this file
        //3. and place the text

        if (d) {

            return d.evaluate({
                script: TaskPaperContext.toString(),
                withOptions: dctOptions
            });
        }

    } 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: '//Inbox and @type=project[-1]',
    filePath: '~/Notes/general.taskpaper',
    textLines: sa.theClipboard()
});

#3

A basic Keyboard Maestro use of this first draft here:

(but the script could also be adapted for sending multi-line clips or jottings)


#4

This works great so far. Thanks!


#5

Here is my solution, using Keyboard Maestro and an AppleScript dialog box:

http://maestromacros.com/downloads/taskpaper-add-a-task/

I designed it for TaskPaper 2, so you will need to update the macro to point to the icon of TP 3.


#6

Added a couple of options to this one:

  • New material to top of Inbox project (rather than bottom)
  • converting contents of tag dates to yyy-mm-dd HH:MM
  • time-stamp of when added


#7

FWIW just made a small tweak so that the dialog check-boxes remember their settings:


#8

Here is a shell script that I use with Launchbar that inserts new items at the top of the first project in the named Taskpaper file. It also automatically saves the file. As you can see this idea does not originate with me and I am grateful to Larry Hynes. All you need to do is save this as a text file with a .sh extension and make it executable using the chmod command, then create a Launchbar search template that calls this executable and you are set.

Please note that one advantage of this script is that neither Taskpaper, nor the file it addresses needs to be open for it to work. The distinct advantage this affords is that you can add an item to your list (I have several different lists) through Launchbar without opening and getting distracted by anything else. Everything stays in the background.

###!/bin/sh

http://larryhynes.net/2013/12/launchbar-to-taskpaper.html

Important disclaimer: I have no idea what I’m doing

TODOFILE=/path/to/your/taskpaper/file
TEMPTODO=$(basename “$0”)
LINE=1
TMPFILE=$(mktemp /tmp/"${TEMPTODO}".XXXXXX) || exit 1
printf “$1” | sed ‘s/^/ - /’ | unexpand -t4 > “$TMPFILE” || exit 1
ed -s $TODOFILE <<EOF
${LINE}r $TMPFILE
w
EOF
rm “$TMPFILE”


#9

Hi all,
I’m a recent convert to TaskPaper and really love - the only thing I’m missing so far is an easy Quick Entry options.
As I use Launchbar mostly I was searching for Launchbar Actions that could do it.

So far I couldn’t find anything readymade - but I found this amazing Launchbar Action to add lines of text to a selected @tag in FoldingText - see image:

Here the maker, Richard Guay explains the how-to (only I really suck at coding, sorry) and here are the LB-actions on Github: https://github.com/raguay/MyLaunchBarActions

Question:

  • Has anyone tried to adjust these actions to Taskpaper?
  • And wouldn’t it then even be possible to choose projects rather than tags? ("“project 1”:, “inbox:”, “world domination:” etc)

Thanks


#10

I can’t answer if anyone else is working on porting these (I am not right now), but I can say that they should be very portable. FoldingText and TaskPaper have very similar scripting models, anything you see done in these scripts should be doable in a similar way in TaskPaper. Unfortunately it won’t be in the “same” way. TaskPaper’s API function is very similar to FoldingText, but the syntax is a bit different. Here’s TaskPaper’s API docs.


#11

Thanks a lot for the reply - it didn’t look to complicated, but sadly I have no clue where to start.
So I guess for me it’s just waiting for an easy quick-entry solution then. THANKS


#12

I’ll take a look at that this week


#13

This is pretty easy using my TaskPaperRuby API, and you won’t even need to have TaskPaper running at the time (or even installed, I suppose).

I use it with Keyboard Maestro. I have a “Prompt for User Input” action in my Keyboard Maestro macro that puts whatever the user enters into a variable called “taskpaper_task_text”. Then add an Execute Shell Script action to run the following script.

Remember that you’ll need to have TaskPaperRuby installed somewhere, and you’ll need to put the path to your TaskPaperRuby installation into the configuration section at the top of the script, as well as the path to whatever TaskPaper document has your “Inbox:” project in it.

#!/usr/bin/ruby

# ==============================================
# Configuration
# ==============================================
tp_ruby_dir = File.expand_path("~/Dropbox/TaskPaper/TaskPaperRuby/src")
inbox_file = File.expand_path("~/Dropbox/TaskPaper/Inbox.taskpaper")
add_at_start = false # false = add at end of inbox project, true = add at start
# ==============================================


km_var = ENV["KMVAR_taskpaper_task_text"]

if km_var and km_var.length > 0
	task_text = km_var
elsif ARGV and ARGV.length > 0
	task_text = ARGV[0]
else
	puts "To create a new task, pass a quoted string as an argument."
	exit
end

require_relative File.join(tp_ruby_dir, 'taskpaperdocument')

inbox_doc = TaskPaperDocument.new(inbox_file)
projects = inbox_doc.all_projects
inbox_projects = projects.select { |proj| proj.title.downcase == "inbox" }

if inbox_projects and inbox_projects.length > 0
	project = inbox_projects[0]
	new_item = TaskPaperItem.new("- #{task_text}")
	project.insert_child(new_item, ((add_at_start) ? 0 : -1))
	inbox_doc.save_file
end

Works fine here. You can also use it directly on the command-line as well as with Keyboard Maestro:

ruby add_to_inbox.rb "New task text here"

Hope that’s helpful to someone.


#14

These solutions are good but what I would want to know is if Jesse is planning to add back this feature in the future. I think of this as a basic feature of TaskPaper which should be included.


#15

I would like to provide some solution to the problem of quick entry. But I’m not sure what I want it to look/work like. Anyway it’s on my list, but not something that I’ve yet started, and I’ve got quite a few other things above it right now.


#16

Have you ever thought about releasing something like a “power pack” for TaskPaper? I am thinking of something that allows quick entries. Also have the same quick entry interface integrate and interact with scripts in a more user friendly manner? I am not sure if it is a good or bad idea, but it is something I will be more than willing to pay for :smile:


#17

Maybe at some point in future, but unlikely in the near term I think. Right now my big constrain is time.


#18

@complexpoint I love this script! I’m using it with Alfred and it works great! Is there a way after the tasks are added to the inbox that your script could tell TaskPaper to do a “Save” automatically?


#19

Good idea. I’ve made an edit and uploaded a new copy to the Keyboard Maestro site.
You can edit the value of a saveAfterAdding option to true or false.


#20

This is perfect! Thanks so much!