Create a new TaskPaper 3 doc from a list of custom templates

In TaskPaper 3, by default ⌘N creates a new TaskPaper file with a standard Welcome.txt contents.

Here is a ⌘⌥N macro which:

  1. Presents a choice of templates from a user-created folder, and
  2. creates a new document based on the chosen template.

(It also shows some basic functions for working with folders and files from JavaScript for Automation:

  • expandTilde :: String -> FilePath
  • pathExistsAndisFolder :: String -> (Bool, Int)
  • listDirectory :: FilePath -> [FilePath]
  • fileType :: FilePath -> UTC String
  • readFile :: FilePath -> IO String)

New TaskPaper 3 doc from chosen template.kmmacros (23.4 KB)

Source of JavaScript for Automation action

// CHOOSE A TEMPLATE FOR A NEW TASKPAPER 3 DOCUMENT

// 1. CREATE A FOLDER CONTAINING TWO OR MORE MODEL TASKPAPER DOCUMENTS
// 2. SPECIFY THE PATH OF THE FOLDER IN THE templatePath VARIABLE
// 3. RUN MACRO ( requires TaskPaper 3 https://www.taskpaper.com/ )

// Ver 0.2 readFile() function updated for UTF8 characters

(function (dctOptions) {
    'use strict';

    // expandTilde :: String -> FilePath
    function expandTilde(strPath) {
        return strPath.charCodeAt(0) === 126 ? ObjC.unwrap(
            $(strPath)
            .stringByExpandingTildeInPath
        ) : strPath;
    }

    // pathExistsAndisFolder :: String -> (Bool, Int)
    function pathExistsAndisFolder(strPath) {
        var ref = Ref();

        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                strPath, ref) ? {
                'exists': true,
                'isFolder': ref[0] === 1
            } : {
                'exists': false
            };
    }

    // listDirectory :: FilePath -> [FilePath]
    function listDirectory(strPath, fm) {
        var fm = fm || $.NSFileManager.defaultManager;

        return ObjC.unwrap(
                fm.contentsOfDirectoryAtPathError(strPath, null))
            .map(ObjC.unwrap);
    }

    // fileType :: FilePath -> UTI String
    function fileType(strPath) {
        var error = $();

        return ObjC.unwrap(
            $.NSWorkspace.sharedWorkspace
            .typeOfFileError(strPath, error)
        );
    }

    // readFile :: FilePath -> IO String
    function readFile(strPath) {
        var ref = Ref();

        return ObjC.unwrap(
            $.NSString.stringWithContentsOfFileEncodingError(
                strPath, $.NSUTF8StringEncoding, ref
            )
        );
    }

    // MAIN 

    ObjC.import('AppKit');
    var strFolder = dctOptions.templateFolder.trim(),
        strPath = strFolder ? expandTilde(strFolder) : undefined;

    if (strPath && pathExistsAndisFolder(strPath)
        .isFolder) {
        var lstMenu = listDirectory(strPath)
            .filter(function (x) {
                return fileType(
                    strPath + '/' + x
                ) === "com.hogbaysoftware.taskpaper.document";
            });

        if (lstMenu.length > 0) {
            var ui = Application("com.apple.systemuiserver"),
                sa = (ui.includeStandardAdditions = true, ui);

            sa.activate();

            var varResult = sa.chooseFromList(lstMenu, {
                    withTitle: dctOptions.withTitle || "",
                    withPrompt: dctOptions.withPrompt ||
                        "Templates in folder:\n\n" + strFolder +
                        "\n\nChoose:",
                    defaultItems: dctOptions.defaultItems ||
                        lstMenu[0],
                    okButtonName: dctOptions.okButtonName || "OK",
                    cancelButtonName: dctOptions.cancelButtonName ||
                        "Cancel",
                    multipleSelectionsAllowed: dctOptions.multipleSelectionsAllowed ||
                        false,
                    emptySelectionAllowed: dctOptions.emptySelectionAllowed ||
                        false
                }),

                strFile = (varResult ? varResult[0] : undefined);

            if (strFile) {
                var tp3 = Application("com.hogbaysoftware.TaskPaper3"),
                    strTemplatePath = strPath + '/' + strFile;

                tp3.documents.push(
                    tp3.Document({
                        textContents: readFile(strTemplatePath)
                    })
                );
                tp3.activate();
                return strTemplatePath;
            }
        }
    } else return "Folder not found:\n\t" + strPath;

})({
    templateFolder: Application("com.stairways.keyboardmaestro.engine")
        .variables['templatePath'].value(), // e.g. '~/TaskPaper Templates',
    withTitle: 'New TaskPaper doc'
});

Rob, many thanks for sharing these great functions. Very useful.

These seem to work fine:

  • expandTilde :: String -> FilePath
  • listDirectory :: FilePath -> [FilePath]
    • Although the output is NOT a path, it is just the name of the file or folder
    • It also shows hidden folders, which was surprising
  • readFile :: FilePath -> IO String

I'm having a bit of trouble with a few of them, perhaps you can show me the error of my ways. :wink:
I am running Yosemite 10.10.5.

  1. function pathExistsAndisFolder(strPath)
  • Always returns false for 'isFolder'
  • Even though it returns true for 'exists'
  • and it definitely is a folder
  1. fileType(strPath)
  • Always get error
    Error on line 85: TypeError: undefined is not an object (evaluating '$.NSWorkspace.sharedWorkspace')
    on this line:
    $.NSWorkspace.sharedWorkspace

I ran a number of different tests on fileType(strPath), but I ended up with this simple script to demonstrate my problem:

'use strict'

var strPath = "/Users/myusername/Documents/Test"

var fileList = listDirectory(strPath);
console.log(fileList)

//--- RETURNS ---
/* .DS_Store,2014-06-17T21-27-45 sysPref Dock.txt,2014-06-17T21.27.45 sysPref Dock.png,NewFolder,NewFolder2,NewFolder3,NewFolder4,NewFolder5,RICH TEXT File.rtf,SubFolder 1,SymbolicLinker.service */

//--- PULLED FROM YOUR SCRIPT ---
// It causes an error when it calls `fileType()`
var lstMenu = listDirectory(strPath)
    .filter(function (x) {
        return fileType(
            strPath + '/' + x
        ) === "com.hogbaysoftware.taskpaper.document";
    });

//--- RETURNS ERROR ---
/*
Error on line 40: TypeError: undefined is not an object (evaluating '$.NSWorkspace.sharedWorkspace')
*/

// listDirectory :: FilePath -> [FilePath]
function listDirectory(strPath, fm) {
    var fm = fm || $.NSFileManager.defaultManager;

    return ObjC.unwrap(
            fm.contentsOfDirectoryAtPathError(strPath, null))
        .map(ObjC.unwrap);
}


// fileType :: FilePath -> UTI String
function fileType(strPath) {
		var error = $();

		return ObjC.unwrap(
				$.NSWorkspace.sharedWorkspace
				.typeOfFileError(strPath, error)
		);
}

I also tried calling fileType() directly with a known fixed path to an existing file:

pathFull = "/Users/myusername/Documents/Test/RICH TEXT File.rtf"
fileType(pathFull)

and got the same exact error.

What am I doing wrong?

Good questions – enterprising of you to start testing them immediately.

The thing to notice about pathExistsAndisFolder is its return type, which I think you are understandably assuming to be a simple boolean. Things to inspect: the type signature comment line preceding the function, the return section, and the point in the macro code in which (one of its) return value(s) is tested.

The fileType() dependency on the ObjC.import('AppKit') line is, of course harder to spot. Use of the $.NSWorkspace object requires that import.

Good luck !

UPDATE

I read your post too quickly on the fileExistsAndisFolder function – forgive me.

Would like to post your test code, with a sample of a path string that you are using ? Hard to comment without seeing that.

Good macro for creating template. However, I was looking for a macro to show similar kind of dialog box with all the taskpaper files in a particular folder to open it.

Has anyone create such macro?

What’s the key element that you are looking for beyond a Finder view of a folder ?

Is it the filtering of which files are shown ?

Currently a finder view of a folder is what I’m currently using which is assigned to a hotkey.

This works perfectly but looking for something minimal. I don’t need any filtering.

something minimal

Visually ?

Not quite sure that I have understood yet.

Visually yes. Finder comes with sidebar, toolbar etc. and the taskpaper extension at the end makes it little more effor to read the filename.

LaunchBar action, perhaps ?

When I put the ObjC.import('AppKit') statement at the top of the script, then fileType() worked fine.

I have found that it is most helpful to users/readers of my scripts if I make sure to state in the function any dependencies and/or versions of software that are required.

I have also found it to improve readability to put the main script on top with the functions on bottom. In this case it would have made the ObjC.import('AppKit') statement standout and I likely would have caught that it was needed in the functions (or some of them).

That would be a bad assumption on your part.
It was clear that it returned an object, and the Results panel of Script Editor clearly showed that. So there was no confusion on my part about the return value.

Here's my test script:

'use strict'

ObjC.import('AppKit');

var pathStr = "~/Documents/Test"

var pathFull = expandTilde(pathStr)
console.log(pathFull)

pathExistsAndisFolder(pathFull)

//~~~~~~~~~~~~~~~~~~~~~~~ END OF MAIN SCRIPT ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


    // expandTilde :: String -> FilePath
    function expandTilde(strPath) {
        return strPath.charCodeAt(0) === 126 ? ObjC.unwrap(
            $(strPath)
            .stringByExpandingTildeInPath
        ) : strPath;
    }

    // pathExistsAndisFolder :: String -> (Bool, Int)
    function pathExistsAndisFolder(strPath) {
        var ref = Ref();

        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                strPath, ref) ? {
                'exists': true,
                'isFolder': ref[0] === 1
            } : {
                'exists': false
            };
    }

It Returns:

/* /Users/username/Documents/Test */
Result:
{"exists":true, "isFolder":false}

As you can see the object["isFolder"] is returning false even though the path is actually a folder.

I suspect the issue may be the OS X version. I am running Yosemite 10.10.5, and I believe you are running El Capitan.

OTOH, maybe it is not a Yosemite issue.

I just found this code on the Apple JXA Release Notes for 10.10, in the Explicit Pass-by-Reference section.
(sorry, link won't work here. see at bottom)

This script works fine on Yosemite. It finds the folder (directory) that your script says is NOT a directory.

'use strict'

ObjC.import('AppKit');


var myPath = "/Users/username/Documents/Test"

testExplicitPassByReference(myPath)

//~~~~~~~~~~~~~~ END OF TEST MAIN ~~~~~~~~~~~~~~

// --- FUNCTION FROM APPLE JXA RELEASE NOTE FOR OS X 10.10 ---
//			(had to correct errors:  added the "var" for all the variables)

function testExplicitPassByReference(path) {
    var ref = Ref()
    var fm = $.NSFileManager.defaultManager
    var exists = fm.fileExistsAtPathIsDirectory(path, ref)
    if (exists) {
        var isDirectory = ref[0]
        return (path + ' is ' + (isDirectory ? '' : 'not ') + 'a directory; ')
    }
    else {
        return (path + ' does not exist\n')
    }
}

RETURNS:

Result:
"/Users/username/Documents/Test is a directory; "

Here's the link to the JXA Release Notes:

I have found ...
I have also found ...

:smile:

Any ideas why the Apple function works, but your pathExistsAndisFolder function does not work, at least not on my system?

Updated the macro (start of thread) to allow for UTF8 (non Anglo-Saxon) characters.

The readFile function is now:

     // readFile :: FilePath -> IO String
    function readFile(strPath) {
        var ref = Ref();

        return ObjC.unwrap(
            $.NSString.stringWithContentsOfFileEncodingError(
                strPath, $.NSUTF8StringEncoding, ref
            )
        );
    }

I've been trying for far too long to get this script working. It's perhaps my novice level in Javascript. I'd appreciate someone to point out where things are going wrong. I'm getting a lot of Type Errors when I go to the debugger which leads me to believe that the conversion between string and Path names is the problem.

I realize this is a long time ago. I just had wanted this functionality and thought it might be a good place to learn some JXA skills. It's proven a much harder nut to crack than I thought.