Appending Items to a TaskPaper File Using Siri


#1

I’ve been trying to find a way to append items to a TaskPaper 3 file using Siri. I’m just looking for a simple way to speak into Siri and have it appended to a file of my choice, preferably at the top.

I’ve looked at IFTTT and a few other ways but none seem to work for me. Does anyone have a solution that might work?


#2

After doing a lot of research and trial and error, I found a solution that works. I used an IFTTT recipe that appended to a Listacular text file. I was able to change the file name and location to one of my TaskPaper files, add the dash and space, and add a tag.

It seems to work pretty well, except that there is a delay unless I open IFTTT.

I had tried numerous IFTTT recipes for Editorial, TaskMator, and SimpleNote but none seemed to work the way I wanted.

I’ll report back if this does not work as expected in the future, or if I find a better solution.


#3

Anyone come up with a solution for Siri reminders into TaskPaper? I added two reminders via Siri hours ago and IFTTT still hasn’t synced it into my special inbox file. Seems like that is a dead end.

I’m very reliant on Siri integration with my Apple Watch to quickly capture tasks I think of. Currently OmniFocus automatically imports these without issues.


#4

Here’s a work in progress for importing reminders from Reminders.app into TaskPaper. Reminders.app is the only one that can get Siri reminders, so I think all apps solve this problem by importing from it.

  • @done Converts Reminders from a specified list into lines of text.
  • @todo Import that text into a TaskPaper document.
  • @todo Delete imported reminders from Reminders.app @complexpoint ?
var Reminders = Application('Reminders')
var remindersSpecifier = Reminders.lists.byName("Boots").reminders
var incompleteReminders = remindersSpecifier.whose({completed:false})()
var lines = []

incompleteReminders.forEach(function (reminder) {
  var properties = reminder.properties()
  var itemText = "- " + properties["name"]

  var dueDate = properties["dueDate"]
  if (dueDate) {
    itemText += " @due(" + dueDate.toISOString() + ")"
  }

  var priority = properties["priority"]
  if (priority) {
    itemText += " @priority(" + priority + ")"
  }
  
  lines.push(itemText)
  
  var body = properties["body"]
  if (body) {
    body.split("\n").forEach(function (each) {
      lines.push("\t" + each)
    })    
  }

  // Todo delete reminder @complexpoint how do I do this? :smile:  
})

lines.join("\n")

// Todo import text into TaskPaper document

#5

Here is what I believe I used. It is a recipe from IFTTT.

All of my TP files are in dropbox, so this worked for me. I had to change the file name to lowercase for some reason if I remember correctly. Then I changed Content section to:

  • {{Title}}. Created: {{CreatedDate}} @siri

That gave me the dash and space, and a @siri tag so I could filter by that to ensure everything got the appropriate tags once in TP.

Then I mad a list in Siri called To Do, and it all works.

I hope I didn’t miss any steps as I did this a while back. I will have to try Jesse’s idea also to see how it works.


#6

I’ll take a look at the scripting route tomorrow : - )


#7

Ahh, the IFTTT integration does require a lowercase filename in Dropbox for the Reminders integration to work. Although IFTTT doesn’t cleanup Reminders after the fact, which is not the best solution.

I’m actually trying this by taking a different route. Launch the Drafts app on iOS and it now pulls in Reminders automatically (it’s a setting) as Drafts, and then using a Drafts action to prepend them into my TaskPaper paper file.

It’s not automatic, but it’s a start. Maybe the above script, when the remaining @todo’s are addressed, will be come an option.


#8

Chris, I had tried Drafts at first, but something didn’t work as well as I wanted. Can’t remember exactly what. I am open to trying again because you are right it does not delete the Reminders.

I still need to test Jesse’s idea to see how that works.

Hopefully between us we can find the right solution.


#9

Here is a rough draft of Jesse’s approach.

At the bottom of the script, you will find some options, including for:

  • The file path of the target TaskPaper 3 file, and
  • the name of the Reminders list to transfer.

If you don’t want it to check for successful transfer and then delete the Reminders.app copy, then edit the value of deleteReminderAfterImport to false.

If useListName is true then the items will be transferred to a TaskPaper 3 project with the same name as the Reminders.app list.

If useListName is false, and projectName is a string, rather than undefined, then the items will go to a project of that name (found or created if necessary)

If useListName is false and projectName is undefined, then the items will simply be sent to the start of the file if atStart is true, and to the end of the file if atStart is false.

The TaskPaper file can be closed or open in TaskPaper 3. If it is open, then there may be a second or two of delay before the display is refreshed and you see the imported items.

It’s a rough first draft, so do experiment and test before adopting.

OPTIONS EXTRACTED FROM FOOT OF SCRIPT:

})({
    RemindersListName: 'Scripting',
    TaskPaper3FilePath: '~/Notes/old notes.taskpaper',

    projectName: undefined, // Project to find or create (to hold imports)
    useListName: true, // Use Reminders list name for above
    atStart: true, // start or end of file if using neither project nor list name ?

    deleteReminderAfterImport: true
});

THE FULL SCRIPT:

// DRAFT 0.03

// Import Reminders from specified list
// adding to start or end of TaskPaper 3 file,
// or to start of a named project

// See options at end of script

(function (dctOptions) {
    'use strict';

    // priorityName :: Int -> String
    function priorityName(intLevel) {
        if (intLevel > 6) return 'low';
        else if (intLevel < 4) return 'hi';
        else return 'med';
    }

    // Date -> String
    function fmtTP(dte) {
        var s = dte.toISOString(),
            d = s.substring(0, 10);

        return dte.getMinutes() ? d + ' ' + s.substring(11, 16) : d;
    }

    // fileExists :: String -> Bool
    function fileExists(strPath) {
        var fm = $.NSFileManager.defaultManager,
            error = $();
        fm.attributesOfItemAtPathError(
            ObjC.unwrap($(strPath)
                .stringByExpandingTildeInPath),
            error
        );
        return error.code === undefined;
    }

    // readFile :: FilePath -> IO String
    function readFile(strPath) {
        return ObjC.unwrap($.NSString.stringWithContentsOfFile(strPath));
    }

    //writeFile :: FilePath -> String -> IO ()
    function writeFile(strPath, strText) {
        $.NSString.alloc.initWithUTF8String(strText)
            .writeToFileAtomicallyEncodingError(
                strPath, false,
                $.NSUTF8StringEncoding, null
            );
    }

    // concatMap :: (a -> [b]) -> [a] -> [b]
    function concatMap(f, xs) {
        return [].concat.apply([], xs.map(f));
    }

    var Reminders = Application('Reminders'),
        lstMatch = Reminders.lists.whose({
            _match: [ObjectSpecifier()
                .name, dctOptions.RemindersListName]
        }),
        list = lstMatch.length > 0 ? lstMatch[0] : undefined;

    if (list) {
        var strPath = $(dctOptions.TaskPaper3FilePath)
            .stringByExpandingTildeInPath.js;

        if (fileExists(strPath)) {
            var blnAtStart = dctOptions.atStart,
                oReminders = list.reminders,
                lstRem = oReminders.whose({
                    _match: [ObjectSpecifier()
                .completed, false]
                })(),

                lstTodo = concatMap(function (r) {
                    try {
                        var strBody = r.body(),
                            dteDue = r.dueDate(),
                            lngHeat = r.priority();
                    } catch (e) {
                        return [];
                    }

                    return [{
                        id: r.id(),
                        lines: [[
                            '- ',
                            r.name(),
                            dteDue ? ' @due(' + fmtTP(dteDue) + ')' : '',
                            lngHeat ? ' @priority(' + priorityName(lngHeat) + ')' : ''
                        ].join('')].concat(
                            strBody ? strBody.split(
                                /[\n\r]+/)
                            .map(function (para) {
                                return '\t' + para;
                            }) : []
                        )
                    }];
                }, lstRem);

            if (lstTodo.length > 0) {

                var strProject = dctOptions.projectName,
                    strHeader = strProject ? strProject : (
                        dctOptions.useListName ? list.name() : ''
                    ),
                    strLabel = strHeader + ':';

                if (strHeader && strHeader.length) {
                    // ADD AFTER HEADER, CREATING IF NOT FOUND

                    var rgxProj = new RegExp(
                            '^(\\s*)' + strHeader + ':.*$',
                            'mi'
                        ),
                        xs = readFile(strPath)
                        .split(/[\n\r]/),
                        i = xs.length,
                        dctProj,
                        x, m;

                    while (i--) {
                        if (xs[i].indexOf(strLabel) !== -1) {
                            m = xs[i].match(rgxProj);
                            if (m) {
                                dctProj = {
                                    indent: m[1].replace(
                                            /    /, '\t'
                                        )
                                        .length,
                                    pre: xs.slice(0, i + 1),
                                    post: xs.slice(i + 1)
                                }
                                break;
                            };
                        }
                    }

                    if (!dctProj) {
                        dctProj = {
                            indent: 0,
                            pre: (blnAtStart ? [] : xs)
                                .concat(strLabel),
                            post: blnAtStart ? xs : []
                        }
                    }

                    var strIndent = Array(dctProj.indent + 2)
                        .join('\t'),
                        strNew = dctProj.pre.join('\n') + '\n' +
                        lstTodo.map(function (x) {
                            return x.lines.map(
                                function (para) {
                                    return strIndent + para;
                                })
                            .join('\n');
                        })
                        .join('\n') + '\n' +
                        dctProj.post.join('\n');

                    writeFile(
                        strPath,
                        strNew
                    );


                    // SIMPLY ADD TO START OR END OF FILE
                } else {

                    var lstUpdate = [
                        lstTodo.map(function (x) {
                            return x.lines.join('\n');
                        })
                        .join('\n'),
                        '\n\n',
                        readFile(strPath)
                    ];

                    writeFile(
                        strPath,
                        (blnAtStart ? lstUpdate :
                            lstUpdate
                            .reverse())
                        .join('')
                    )
                }

                // IF REQUIRED, CHECK THE IMPORT AND DELETE FROM REMINDERS.APP

                if (dctOptions.deleteReminderAfterImport) {
                    var strUpdated = readFile(strPath);

                    return concatMap(function (dct) {
                        var strName = dct.lines[0];

                        if (strUpdated.indexOf(strName) !== -1) {
                            Reminders.delete(
                                oReminders.byId(dct.id)
                            );
                            return [strName];
                        } else {
                            return [];
                        }
                    }, lstTodo);
                }

            } // lstTodo.length > 0

        } else return 'File not found: ' + dctOptions.TaskPaper3FilePath;

    } else return 'No list named "' + dctOptions.RemindersListName +
        '" found in Reminders';

})({
    RemindersListName: 'Scripting',
    TaskPaper3FilePath: '~/Notes/old notes.taskpaper',

    projectName: undefined, // Project to find or create (to hold imports)
    useListName: true, // Use Reminders list name for above
    atStart: true, // start or end of file if using neither project nor list name ?

    deleteReminderAfterImport: true
});

#10

Rob, you are my hero. Just wanted to let you know I appreciate your efforts!


#11

It’s all Jesse’s work :slight_smile:

( there’s a kind of brilliance in this model of a tagged and deeply filterable plain text outline with such a rich scripting API )


#12

Yeah, I am grateful to him too, but at least I can pay him for his heroics!


#13

I have to disagree with that, but thanks for all the scripts!


#14

Most things, even bread, are improved by sharing them.

( With scripts, you get better ideas from others, and bug reports too : - )


#15

This is such a great functionality to have, since it allows me to braindump stuff on the go to my master taskpaper document. The only problem I’ve encountered is that each time the script is run, the taskpaper document completely collapses all items. This becomes a big pain in larger multi-project documents. Do you guys know if there’s anyway around this?


#16

I think this should not happen as often in the latest 3.5 preview release?


#17

I think I’m on the latest release “Version 3.5 Preview (266)” and it occurred when I ran the script. Granted my document was open at the same time as I ran the script. Could that have something to do with it?


#18

Thanks, I just gave it a try and I think there are two issues preventing it from working.

First I wasn’t correctly restoring the fold metadata when a document was changed outside Taskpaper while opened in TaskPaper. I’ve fixed that for the next release.

@complexpoint Second in the script the part where the file is actually written to disk needs to be changed to this I think:

    function writeFile(strPath, strText) {
        $.NSString.alloc.initWithUTF8String(strText)
            .writeToFileAtomicallyEncodingError(
                strPath, false,
                $.NSUTF8StringEncoding, null
            );
    }

The particular change that I made was to pass in “false” for “atomically”. It seems that when you write a file atomically with that method it also erases all extended attribute information (which is where I store the folded items). There might be some other API that writes atomically and preservers, but this was the first fix that found.

So with that change (and the next preview release) I think the those folded items should be preserved.


#19

Wow. Such fast response on this issue. And I really appreciate the focus you are giving on retaining the folding between files and sessions as it helps maintaining large files and keeps the user focused across files.

You’re killing it with 3.5, Jesse!


#20

Thank you !

I’ll update that in my library too.

Sounds like I’d better see if any other scripts were writing atomically …