Hoping for a couple of scripts


#1

Greetings! I’m a major Javascript noob. I was able to do some stuff with TextExpander Javascript, but that API is apparently much simpler because I really can’t wrap my head around this one. I’m hoping that someone already has (or that it wouldn’t be too terribly difficult to make) a script for…

  • Cutting the descendants of a specific file’s “Archive” project and appending them to an Archive file

and

  • Cutting the entire contents of one file and appending it to a specific file’s “Inbox” project

Hoping someone can help! Thanks. :relaxed:


#2

I guess those two mappings could be variously defined and packaged:

  • From Archive project to a file (in an open target document for a visual check ? Straight to a file somewhere ? Launched automatically by something like Hazel ? Manually ?
  • From one file to the Inbox project of another – similar questions, plus things like what happens to the denuded file ? And how do we identify the target file ?

#3

Good questions. Ones I didn’t quite think about; the core logic would be mostly the same, wouldn’t it?

  • Archive project to file
    This one I was thinking would be triggered manually (eg keyboard maestro) from my main ‘TODO.taskpaper’ document. I would have the TODO file open, but ideally wouldn’t need to have the Archive file open.
  • Inbox file to project
    Ideally, the file wouldn’t need to be open. It would just be triggered by Hazel whenever Hazel found that the file had any contents, copy the contents to the Inbox project, remove the contents of the inbox file, and be done with it. File would still exist, would just be empty. Target file would be identified in the script specifically.

#4

I’ve made a first sketch of one of these – Archive project to file.

NB Test properly with dummy data before you use it with real work

It won’t work until you have set a couple of options at the end of the script:

{
    archiveFilePath: '~/Desktop/archiveFile.taskpaper',

    // Only set removeFromMainFile to true when you
    // are satisfied that the script is successfully appending
    // material to your named archive file

    removeFromMainFile: false
}
  1. You need to create an Archive file and give a full path to it. (You can abbreviate with ~ if you want)
  2. It won’t remove things from the Archive project of the active TaskPaper 3 document until you have checked that the append to your other file is working, and you have edited the value of removeFromMainFile: to true

JavaScript for Automation draft source

// ROUGH DRAFT 0.01

// Intended to clear all descendants of any Archive: project
// in the active TaskPaper 3 document
// appending them to a specified archive file

// Test before using

// See options at end of script


(function (dctOptions) {
    'use strict';

    // TASKPAPER CONTEXT


    // archivedLines :: Editor -> String
    function archivedLines(editor) {
        var lstArchived = editor.outline
            .evaluateItemPath(
                '//Archive and @type=project//*'
            );

        return lstArchived.length ? ItemSerializer
            .serializeItems(
                lstArchived,
                undefined,
                ItemSerializer.TEXTMimeType
            ) : '';
    }

    // clearArchive :: Editor -> ()
    function clearArchive(editor) {
        var outline = editor.outline,
            lstArchived = outline
            .evaluateItemPath(
                '//Archive and @type=project//*'
            );

        if (lstArchived.length) {
            outline.groupUndoAndChanges(function () {
                outline.removeItems(lstArchived);
            });
        }
    }



    // JAVASCRIPT FOR AUTOMATION CONTEXT

    // fileExists :: String -> Bool
    function fileExists(strPath) {
        var fm = $.NSFileManager.defaultManager,
            error = $();

        return (
            fm.attributesOfItemAtPathError(
                ObjC.unwrap($(strPath)
                    .stringByStandardizingPath),
                error
            ),
            error.code === undefined
        );
    }

    var tp3 = Application('com.hogbaysoftware.TaskPaper3'),
        ds = tp3.documents,
        d = ds.length ? ds[0] : undefined,
        maybeStrArchived = d ? d.evaluate({
            script: archivedLines.toString(),
        }) : undefined;


    if (maybeStrArchived) {
        var se = Application('System Events'),
            sea = (se.includeStandardAdditions = true, se),
            a = Application.currentApplication(),
            sa = (a.includeStandardAdditions = true, a);

        var strPath = ObjC.unwrap(
            $(dctOptions.archiveFilePath)
            .stringByStandardizingPath
        );

        // Find or create the output file
        if (fileExists(strPath)) {
            var varError = sa.doShellScript(
                'echo "' + maybeStrArchived +
                '" >> "' + strPath + '"'
            );

            if (varError) {
                sea.activate();
                sea.displayDialog(varError);

                // If no error was returned from the append
                // and the user has set the option to clear the
                // archived items from the main file, then
                // remove them (undoable with Cmd Z)
            } else {
                if (dctOptions.removeFromMainFile === true) {
                    d.evaluate({
                        script: clearArchive.toString(),
                    })
                }
            }

        } else {
            // Warn that the specified archive file was not found,
            // and end the script
            return (
                sea.activate(),
                sea.displayDialog(
                    'No file found at:\n\n\t' +
                    strPath, {
                        buttons: ['OK'],
                        defaultButton: 'OK',
                        cancelButton: 'OK',
                        withTitle: 'Archive to file',
                        withIcon: sa.pathToResource(
                            'TaskPaperAppIcon.icns', {
                                inBundle: 'Applications/TaskPaper.app'
                            }
                        ),
                        givingUpAfter: 30
                    }
                ),
                false
            )
        }

        // append to the file
        // check that the archive file contains the new material
        // delete it from the open document
    }

})({
    archiveFilePath: '~/Desktop/archiveFile.taskpaper',

    // Only set removeFromMainFile to true when you
    // are satisfied that the script is successfully appending
    // material to your named archive file

    removeFromMainFile: false
});

#5

So the second one (Inbox File to Project) presumably takes two file path arguments ?

  • Feeder file which will be emptied
  • Main file containing an inbox

If the Hazel-triggered script found that the main file was closed, would it open it, add new material to the Inbox and then close it again ?


#6

Wow, thank you for putting that together! That’s very gracious of you.

Do you have any recommendations on where I can go to learn what all the points in that script mean? I know the basics of variables and strings and functions and ‘if else’ and whatnot, but once it gets into the specifics of all of those I start to get lost.

As for your last question… I was hoping Hazel would be able to do that without opening any files? But I wouldn’t be terribly upset if it weren’t able to do that.


#7

where I can go to learn what all the points in that script mean?

On JavaScript generally:

On the Automation library (the JavaScript for Automation Context):

Especially the basics in the 10.10 notes:
https://developer.apple.com/library/mac/releasenotes/InterapplicationCommunication/RN-JavaScriptForAutomation/Articles/Introduction.html

and:

and, of course, on the TaskPaper 3 context:
http://guide.taskpaper.com/creating_scripts.html
http://guide.taskpaper.com/scripting_api.html

I was hoping Hazel would be able to do that without opening any files

Not impossible, but then the script would not be able to make use of the scripting interface which Jesse has provided.

It would have to use a regex to look for a pattern which matched an Inbox project, split the text file just after the Inbox line, and reassemble the three parts (with the right level of indentation prepended to all the incoming lines). Manageable if you are happy for the new material to go at the top of the Inbox, but possibly not worth the scripter time for more fiddly and ambitious things, like appending new material at the end of an Inbox list. (So sequencing of incoming material might lose its order in time).

(appending to end of Inbox is easy through Jesse’s API, if the file has been loaded into TaskPaper 3)


#8

Fair enough. I think for this one I might try to tackle it on my own. It’ll be good for me. :smile: Thanks again, I very much appreciate the time and help! I’ll start going over the resources you recommended. I think I’d come across them before, hoping there would be an easier way… looks like I just need to put in the work, haha.


#9

Have fun !

(and don’t hesitate to ask about any obscurities)

I guess one approach might be to use the TaskPaper 3 interface, but delegate some of the logic to the launching context (Hazel etc) so that:

  1. If Hazel-launched script finds that TaskPaper 3 isn’t running it gives up for the moment
  2. Your script is automatically launched not only when a file changes but also when TaskPaper 3 first starts (checking for and ingesting any new external material for the Inbox at the start of the editing session)