Change the welcome text

I like the script approach for now too. Maybe someday I would make a template document in the application support folder, but I think I’d rather live with the current “copy paste” favorite searches approach for a while. I think in the end it might make more sense to add some “built in” searches (that can be edited in preferences or something) so that you can use saved searches without embedding them in your doc. Then you could just used in document saved searches for very specific cases.

A Keyboard Maestro ( ⌘⌥N ) version here:

Hey guys, the KM script above works great however it creates a new file instead of appending the template to an existing file (it’s listed on the TP wiki as “A script to append templates in a folder to document”).

Has anyone tried to modify it to make it append to an existing file instead of creating a new file?
Another idea would be to have it add the contents of the template to the open document (perhaps to wherever the text cursor is).

My goal would be to have template for projects / searches that I could add to larger taskpaper files instead of having templates for entire files.

Thanks – I hadn’t seen that labelling of the script: amended now.

Would it work for you to add a button to Copy the text of the selected template into the clipboard ?

I guess that’d be quite useful because I could just add another action to the KM macro to make it immediately paste the clipboard to wherever my cursor is at. :slight_smile:

For a similar macro which copies the contents of the template to clipboard (rather than creating a new document with it), you can use a lightly modified version of the code in the Keyboard Maestro Execute JavaScript for Automation action:

( Or, of course, use something like Typinator )

JavaScript for Automation source:

// CHOOSE A TEMPLATE AND COPY ITS TEXT TO CLIPBOARD

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

(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 -> UTC 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,
        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 ||
                        "Copy file contents",
                    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,
                    strText = readFile(strTemplatePath);

                sa.setTheClipboardTo(
                    strText
                );

                tp3.activate();

                return strText;
            }
        }
    } else return "Folder not found:\n\t" + strPath;

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

Thanks @complexpoint! This works great!

While this is not a huge problem, I’d just like to point out that the script doesn’t deal that well with special characters such as ç, ã, ó, etc.
For example, if a template contains the word “documentação” (portuguese for documentation), the script will copy it as “documenta√ß√£o”.
This is not a deal breaker however I believe other users might welcome a fix for this issue as well.

Obrigado !

( good catch – I’ve updated the readFile() function in the two scripts above to be a bit less parochially Anglo Saxon )

Should work OK now, I think.

@complexpoint Yeap! That fixed it!

Obrigado! :stuck_out_tongue_winking_eye:

Hi
Does this script works on Big sur ?
Got an error that the path is not found with the KeyboardMaestro version

Thanks
Patrick

I think you should just have to edit the line:

var tp3 = Application("com.hogbaysoftware.TaskPaper3")

replacing the string:

"com.hogbaysoftware.TaskPaper3"

with just

"TaskPaper"

1 Like

Thanks a lot @complexpoint
It do not works
However I have found a workaround using keyboard maestro and it do the job
Thanks again

Thanks – I’ll take a look over the weekend.

UPDATE

It looks as if we need to adapt to a change in the document.textContents property:

The following is a rewrite which is working here on Big Sur:

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // 1. Create a folder containing two or more
    //    model TaskPaper documents
    // 2. Edit the value of `fpTemplateFolder` at the top
    //    of this script to match the path the folder.
    // 3. Run this script
    //    (e.g. from a Keyboard Maestro or Fastscripts shortcut)

    ObjC.import("AppKit");

    // TaskPaper 3 – new file from menu of templates.
    // Ver 2.7

    // Rob Trew @2021

    const fpTemplateFolder = "~/TaskPaper Templates";

    const templateExtension = ".taskpaper";

    const title = "TaskPaper Templates";

    // main :: IO ()
    const main = () => {
        const
            fpFolder = filePath(fpTemplateFolder),
            extension = templateExtension
            .startsWith(".") ? (
                templateExtension
            ) : `.${templateExtension}`;

        return either(
            msg => alert(title)(msg)
        )(
            chosenTemplateName => chosenTemplateName
        )(
            bindLR(
                doesDirectoryExist(fpFolder) ? (
                    Right(fpFolder)
                ) : Left(`Folder not found: ${fpFolder}`)
            )(folderPath => {
                const
                    templateNames = folderTemplates(
                        fpFolder
                    )(
                        extension
                    )
                    .sort(),
                    intFiles = templateNames.length;

                return bindLR(
                    0 < intFiles ? (
                        1 < intFiles ? (
                            showMenuLR(false)(title)(
                                "Choose a template:"
                            )(templateNames[0])(
                                templateNames
                            )
                        ) : Right([templateNames[0]])
                    ) : Left(
                        `No '${extension}' templates` + (
                            `\n\nfound in:\n\t${fpFolder}`
                        )
                    )
                )(
                    chosenTemplateOpened(folderPath)(
                        extension
                    )
                );
            })
        );
    };


    // folderTemplates :: FilePath -> String -> [String]
    const folderTemplates = fpFolder =>
        extension => listDirectory(fpFolder)
        .flatMap(fileName => {
            const [base, ext] = Array.from(
                splitExtension(fileName)
            );

            return extension !== ext ? (
                []
            ) : Boolean(base) ? (
                [base]
            ) : [];
        });


    // chosenTemplateOpened :: FilePath -> [FileName]
    // -> IO FilePath
    const chosenTemplateOpened = fpFolder =>
        extension => choices => {
            const
                taskpaper = Application("TaskPaper"),
                fpTemplate = filePath(combine(fpFolder)(
                    `${choices[0]}${extension}`
                ));

            return bindLR(
                readFileLR(fpTemplate)
            )(
                txt => {
                    // showLog("Template path:", fpTemplate);
                    // showLog("Template content:", txt);

                    const newDoc = taskpaper.Document({
                        textContents: txt
                    });

                    // In TaskPaper,
                    return (
                        taskpaper.activate(),
                        taskpaper.documents.push(newDoc),
                        // newDoc.textContents = txt,
                        // newDoc.evaluate({
                        //     script: `${TaskPaperContext}`,
                        //     withOptions: {
                        //         textContents: txt
                        //     }
                        // }),
                        // in the JS interpreter.
                        Right(fpTemplate)
                    );
                }
            );
        };


    // ---------------- TASKPAPER CONTEXT ----------------
    // const TaskPaperContext = (editor, options) =>
    //     editor.outline.reloadSerialization(
    //         options.textContents
    //     );


    // ----------------------- JXA -----------------------

    // alert :: String => String -> IO String
    const alert = alertTitle =>
        s => {
            const
                sa = Object.assign(
                    Application("System Events"), {
                        includeStandardAdditions: true
                    });

            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: alertTitle,
                    buttons: ["OK"],
                    defaultButton: "OK"
                }),
                s
            );
        };


    // showMenuLR :: Bool -> String -> String ->
    // [String] -> String -> Either String [String]
    const showMenuLR = blnMult =>
        // An optionally multi-choice menu, with
        // a given title and prompt string.
        // Listing the strings in xs, with
        // the string `selected` pre-selected
        // if found in xs.
        menuTitle => prompt => selected => xs =>
        0 < xs.length ? (() => {
            const sa = Object.assign(
                Application("System Events"), {
                    includeStandardAdditions: true
                });

            sa.activate();

            const v = sa.chooseFromList(xs, {
                withTitle: menuTitle,
                withPrompt: prompt,
                defaultItems: xs.includes(selected) ? (
                    [selected]
                ) : [xs[0]],
                okButtonName: "OK",
                cancelButtonName: "Cancel",
                multipleSelectionsAllowed: blnMult,
                emptySelectionAllowed: false
            });

            return Array.isArray(v) ? (
                Right(v)
            ) : Left(`User cancelled ${title} menu.`);
        })() : Left(`${title}: No items to choose from.`);


    // --------------------- GENERIC ---------------------

    // Left :: a -> Either a b
    const Left = x => ({
        type: "Either",
        Left: x
    });


    // Right :: b -> Either a b
    const Right = x => ({
        type: "Either",
        Right: x
    });


    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2
        });


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = m =>
        mf => m.Left ? (
            m
        ) : mf(m.Right);


    // combine (</>) :: FilePath -> FilePath -> FilePath
    const combine = fp =>
        // Two paths combined with a path separator.
        // Just the second path if that starts with
        // a path separator.
        fp1 => Boolean(fp) && Boolean(fp1) ? (
            "/" === fp1.slice(0, 1) ? (
                fp1
            ) : "/" === fp.slice(-1) ? (
                fp + fp1
            ) : `${fp}/${fp1}`
        ) : fp + fp1;


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => e.Left ? (
            fl(e.Left)
        ) : fr(e.Right);


    // doesDirectoryExist :: FilePath -> IO Bool
    const doesDirectoryExist = fp => {
        const ref = Ref();

        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(fp)
                .stringByStandardizingPath, ref
            ) && ref[0];
    };


    // filePath :: String -> FilePath
    const filePath = s =>
        // The given file path with any tilde expanded
        // to the full user directory path.
        ObjC.unwrap(ObjC.wrap(s)
            .stringByStandardizingPath);


    // listDirectory :: FilePath -> [FilePath]
    const listDirectory = fp =>
        ObjC.unwrap(
            $.NSFileManager.defaultManager
            .contentsOfDirectoryAtPathError(
                ObjC.wrap(fp)
                .stringByStandardizingPath,
                null
            ))
        .map(ObjC.unwrap);


    // readFileLR :: FilePath -> Either String IO String
    const readFileLR = fp => {
        // Either a message or the contents of any
        // text file at the given filepath.
        const
            e = $(),
            ns = $.NSString
            .stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );

        return ns.isNil() ? (
            Left(ObjC.unwrap(e.localizedDescription))
        ) : Right(ObjC.unwrap(ns));
    };


    // splitExtension :: FilePath -> (String, String)
    const splitExtension = fp => {
        // A tuple of the basename and the extension,
        // in which the latter includes the "."
        const
            xs = fp.split("."),
            lng = xs.length;

        return 1 < lng ? (
            Tuple(
                xs.slice(0, -1).join(".")
            )(
                `.${xs[lng - 1]}`
            )
        ) : Tuple(fp)("");
    };

    // // showLog :: a -> IO ()
    // const showLog = (...args) =>
    //     // eslint-disable-next-line no-console
    //     console.log(
    //         args
    //         .map(JSON.stringify)
    //         .join(" -> ")
    //     );

    return main();
})();

Hi @complexpoint
Thanks a lot but get this

in my ~/Documents/Template/Taskpaper/" folder I have 2 files with extension .taskpaper
Something goes wrong
Both running from Keyboard maestro or Script editor (using Javascript) same issue

Thanks
Patrick

Forgat to add in the Script editors got this
“No ‘com.hogbaysoftware.taskpaper’ templates\n\nfound in:\n\t/Users/XXX/Documents/Template/Taskpaper”

The first thing to try might be to edit the line (near the top of the script)

const templateUTI = "com.hogbaysoftware.taskpaper";

changing it to something like:

const templateUTI = "public.text";

and then tell us what happens.

Exactly the same as the message change a bit

does the folder must contain more than .taskpaper file into right?
thanks

with:

const fpTemplateFolder = "~/Documents/Template/Taskpaper/";

const templateUTI = "public.text";

const title = "TaskPaper Templates";

Thanks.

Next test (seems to work here):

const templateUTI = "com.hogbaysoftware.taskpaper";

same issue as before
Should I grant Script editor to access the file system?