Change the welcome text

I think it would be a good idea, and beneficiary to all the users of Taskpaper to create a welcome text file within the application to support to change or modify the “Welcome text” of TaskPaper to the user’s liking.

As I was going through some of the search posts, I figure it would be a good idea for me to include certain of those by default. I then modified the welcome text within the app to my liking, but thought that for others it was what some have already accused Taskpaper to be, “too geeky.”

If this is implemented, some people can share their default searches and doing so, educate the beginner user of the many ways power users are using the app. Like Doug said in one of his posts, [quote=“doug, post:7, topic:1628”]
but i found Matt Gemmell’s examples from his blogpost hugely useful
[/quote]

These are examples in a default text Matt Gemmell shared :wink:

1 Like

Perhaps, as a stop-gap – a script or macro to easily back-up, edit or revert:

/Applications/TaskPaper.app/Contents/Resources/Welcome.txt

?

My idea of changing the default “welcome text” was to change it to something that I can use all the time and change every blue moon.

When you proposed having a script or macro to do it, I thought… well, if I could have a folder with several of these “default texts” for certain jobs and I could pick and choose those when I am editing a document, that would be mind blowing awesome :smile: Something like those snippets that someone uses for coding, but for searches, tags, etc.

So perhaps a script which supplements the usual ⌘N with something like ⌘⌥ N for ‘New document based on template’, and simply throws up a list of templates in a folder somewhere ?

(Rather than fiddling with the resources in the .app package, which are refreshed with every update)

Although my original request was to have Taskpaper read a document in the application support folder and use that for new documents when the “Show Welcome Text” option was selected – if such document wasn’t available, then just read the welcomed text included in the .app package – I do like your idea better.

Here is a very rough first sketch – if you would like to test it and suggest adjustments, we could put a version into the script wiki, and perhaps make Keyboard Maestro and LaunchBar etc versions ?

(() => {
    "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.0

    // Rob Trew @2021

    const fpTemplateFolder = "~/TaskPaper Templates";

    const templateUTI = "com.hogbaysoftware.taskpaper";

    const title = "TaskPaper Templates";

    // main :: IO ()
    const main = () => {
        const fpFolder = filePath(fpTemplateFolder);

        return either(
            alert("TaskPaper Templates")
        )(
            chosenTemplateName => chosenTemplateName
        )(
            bindLR(
                doesDirectoryExist(fpFolder) ? (
                    Right(fpFolder)
                ) : Left(`Folder not found: ${fpFolder}`)
            )(folderPath => {
                const
                    templateNames = filesOfTypeInFolder(
                        templateUTI
                    )(folderPath)
                    .map(takeBaseName),
                    intFiles = templateNames.length;

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

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

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


    // filesOfTypeInFolder :: UTI -> FilePath -> [FileName]
    const filesOfTypeInFolder = uti =>
        fpFolder => listDirectory(fpFolder)
        .flatMap(
            fileName => either(
                // Not a valid file path.
                () => []
            )(
                fileUTI => uti !== fileUTI ? (
                    []
                ) : [fileName]
            )(
                fileUTI(
                    combine(fpFolder)(
                        fileName
                    )
                )
            )
        )
        .sort();


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


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

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


    // fileUTI :: FilePath -> Either String String
    const fileUTI = fp => {
        // ObjC.import('AppKit')
        const
            e = $(),
            uti = $.NSWorkspace.sharedWorkspace
            .typeOfFileError(fp, e);

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


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


    // readFile :: FilePath -> IO String
    const readFile = fp => {
        // The contents of a text file at the
        // filepath fp.
        const
            e = $(),
            ns = $.NSString
            .stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );

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

    // takeBaseName :: FilePath -> String
    const takeBaseName = fp =>
        ("" !== fp) ? (
            ("/" !== fp[fp.length - 1]) ? (() => {
                const fn = fp.split("/").slice(-1)[0];

                return fn.includes(".") ? (
                    fn.split(".").slice(0, -1)
                    .join(".")
                ) : fn;
            })() : ""
        ) : "";

    return main();
})();
1 Like

Thank you. I added that to the wiki, and I think that we had enough scripts of this kind to create a new category.

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();
})();