Script to move selected task to other document with same Root Project Name

Hi,
Does anybody have, or can anybody help me to create a script to move a task (no need for child/sub tasks) to an other file on top of the same root Project name?

So the content in the active TaskPaper file is (say file current.taskpaper):

  • Root Project Name:
    • Task item
    • Task item
    • Task item with sub
      • Task item selected <==
      • Task item
    • Task item
  • Other Project Name:
    • Task item

What I want to accomplish is that “Task item selected” is copied, and then the copied item is placed on top in the root of project: “Root Project Name” in a (predefined) other TaskPaper file (say overview.taskpaper). If “Root Project Name” does not exist, the project needs to be generated.

I’ve looked in the script Wiki, but found no solution in order to get the Root Project Name of selected item.

BTW, I own a copy of Keyboard Maestro.

== Thnx, Feek

The name of the target project in the target file is copied from that of the enclosing project in the source file ?

Hi @complexpoint, thnx for the prompt reply!

When “Root Project Name” in the target file exist, the copied task (only task item on cursor, no other tasks) should be placed on ‘top’ in that project (as first child). But when project “Root Project Name” does not exits in target file, the project needs to be created and the task should be place inside that “new” project.

Here’s a first draft (behind the disclosure triangle below).

The simplest approach – in scripting terms – is to let the script open the target document (if it isn’t already open).

Ver 6 (behind the disclosure triangle below), activates the target document, and gives focus to the target project.

(various other approaches to final focus are possible)


You will need to adjust the value of fpTarget (the filePath of the target file) near the top of the script source below.


[Using Scripts](https://www.taskpaper.com/guide/using-taskpaper/using-scripts.html)

Copy the whole of the source behind the disclosure triangle:

JS Source
(() => {
    'use strict';

    // Rob Trew @2020
    // Ver 0.06

    // Updated to allow for proper handling of
    // square brackets in project names.

    // 

    // main :: IO ()
    const main = () => {
        const fpTarget = '~/Desktop/overview.taskpaper';
        const
            taskpaper = Application('TaskPaper'),
            docs = taskpaper.documents;
        return either(
            alert('Problem')
        )(
            ([sourceDoc, targetDoc, projectName, taskName]) => (
                //appWinRaisedByNameLR('TaskPaper')(sourceDoc.name()),
                `${taskName} copied to ${projectName}:\n` + (
                    `in ${targetDoc.file()}`
                )
            )
        )(
            // Are any documents open in TaskPaper ?
            bindLR(
                0 < docs.length ? (
                    Right(docs.at(0))
                ) : Left('No TaskPaper documents open.')
            )(
                // Is any task within a project selected ?
                sourceDoc => bindLR(
                    sourceDoc.evaluate({
                        script: `${tp3SelectedItem}`
                    })
                )(
                    // Can we find or create a file at the given path ?
                    ([projectName, taskText]) => bindLR(
                        doesFileExist(fpTarget) ? (
                            Right(fpTarget)
                        ) : writeFileLR(fpTarget)(
                            `${projectName}:\n`
                        )
                    )(
                        // Can we place a copy of the task in a matching
                        // project in the target file ?
                        fp => {
                            const
                                fpFull = filePath(fp),
                                targetDocs = docs().filter(
                                    x => fpFull === Path(x.file()).toString()
                                ).length,
                                targetDoc = 0 < targetDocs.length ? (
                                    targetDocs[0]
                                ) : (
                                    taskpaper.open(fpFull)
                                );
                            return bindLR(
                                targetDoc.evaluate({
                                    script: `${tp3PlacedInProject}`,
                                    withOptions: {
                                        projectName: projectName,
                                        taskText: taskText
                                    }
                                })
                            )(
                                taskName => Right([
                                    sourceDoc,
                                    targetDoc,
                                    projectName,
                                    taskName
                                ])
                            );
                        }
                    )
                )
            )
        );
    };

    // -------------- TASKPAPER CONTEXT --------------

    // tp3SelectedItem :: Editor -> Either String (String, String)
    const tp3SelectedItem = editor => {
        const main = () => {
            const
                selectedItem = editor.selection.startItem,
                enclosingProjects = selectedItem.ancestors
                .filter(x => 'project' === x.getAttribute('data-type'));
            return bindLR(
                0 < enclosingProjects.length ? (
                    Right(
                        last(enclosingProjects).bodyContentString
                    )
                ) : Left('Selected item not enclosed by a project.')
            )(
                projectName => Right([
                    projectName, selectedItem.bodyString
                ])
            );
        };

        // ----------------- 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 => undefined !== m.Left ? (
                m
            ) : mf(m.Right);

        // last :: [a] -> a
        const last = xs =>
            // The last item of a list.
            0 < xs.length ? (
                xs.slice(-1)[0]
            ) : undefined;

        // tp3SelectedItem :: main
        return main();
    };


    // tp3PlacedInProject :: Editor -> Dict -> 
    // Either String String
    const tp3PlacedInProject = (editor, options) => {
        const main = () => {
            const
                outline = editor.outline,
                project = projectFoundOrCreated(
                    outline
                )(
                    options.projectName
                );
            return (
                outline.groupUndoAndChanges(
                    () => project.insertChildrenBefore(
                        outline.createItem(options.taskText),
                        project.hasChildren ? (
                            project.firstChild
                        ) : undefined
                    )
                ),
                editor.focusedItem = project,
                Right(options.taskText)
            );
        };

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

        // projectFoundOrCreated :: TP3Outline -> 
        // String -> Bool -> TP3Item
        const projectFoundOrCreated = outline =>
            // A reference to a TaskPaper Project item,
            // either found in the outline, or created
            // at the top of it.
            projectName => {
                const
                    k = projectName.trim(),
                    matches = outline.evaluateItemPath(
                        `//project "${k}"`
                    ),
                    blnFound = 0 < matches.length,
                    project = blnFound ? (
                        matches[0]
                    ) : outline.createItem(k + ':');
                return (
                    blnFound || outline.insertItemsBefore(
                        project,
                        outline.root.firstChild
                    ),
                    project
                );
            };
        // tp3PlacedInProject :: main
        return main();
    };

    // ---------- JAVASCRIPT FOR AUTOMATION ----------

    // alert :: String => String -> IO String
    const alert = title => s => {
        const
            sa = Object.assign(Application('System Events'), {
                includeStandardAdditions: true
            });
        return (
            sa.activate(),
            sa.displayDialog(s, {
                withTitle: title,
                buttons: ['OK'],
                defaultButton: 'OK',
                withIcon: sa.pathToResource('TaskPaper.icns', {
                    inBundle: 'Applications/TaskPaper.app'
                })
            }),
            s
        );
    };

    // appWinRaisedByNameLR :: String -> String -> IO String
    const appWinRaisedByNameLR = appName =>
        strName => {
            const
                se = Application('System Events'),
                ps = se.applicationProcesses.where({
                    name: appName
                });
            return bindLR(
                0 < ps.length ? (
                    Right(ps.at(0))
                ) : Left('Application process not found')
            )(proc => {
                const
                    ws = proc.windows.where({
                        name: strName
                    });
                return 0 < ws.length ? (
                    Right((
                        se.perform(ws.at(0).actions.byName('AXRaise')),
                        strName
                    ))
                ) : Left('Window not found by name: ' + strName);
            })
        };

    // --------------------- 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 => undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // doesFileExist :: FilePath -> IO Bool
    const doesFileExist = fp => {
        const ref = Ref();
        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(fp)
                .stringByStandardizingPath, ref
            ) && 1 !== ref[0];
    };

    // 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 => 'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

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

    // writeFileLR :: FilePath -> Either String IO FilePath
    const writeFileLR = fp =>
        s => {
            const
                e = $(),
                efp = $(fp)
                .stringByStandardizingPath;
            return $.NSString.alloc.initWithUTF8String(s)
                .writeToFileAtomicallyEncodingError(
                    efp, false,
                    $.NSUTF8StringEncoding, e
                ) ? (
                    Right(ObjC.unwrap(efp))
                ) : Left(ObjC.unwrap(e.localizedDescription));
        };

    return main();
})();
2 Likes

Wow, great @complexpoint. You make my day!

I’ll dig into this tonight (CET), and hopefully I finally understand all the JS-code.

== Feek

1 Like

Works completely! Thnx!!!

Hi,
one small issue. In some project names I have square brackets, like:

[201217] Some Project Name

I expect in the search something needs to be escaped, because projects are duplicated, so they “were not found”

Interesting :slight_smile:

I think I’ll start by deferring to @jesse’s view on the best approach there.

The problem, I think, is that square brackets (and in particular, square brackets containing numeric strings) have a role in TaskPaper’s itemPath query language, and that means that outline.evaluateItemPath can’t, in the case of those project names, directly match a search of the pattern

//[201217] Some Project Name

Experimenting with ⌘⇧F (View > Begin Editor Search) I notice that simply escaping the square brackets doesn’t immediately allow for a match.

Jesse can probably tell us whether the TaskPaper parser and the itemPath parser both recognise square brackets as valid characters in project names, and if they do, then what the correct escape pattern might be.

Ah … I should have thought of this:

//"[201217] Some Project Name"

I’ve now updated the script (behind the disclosure triangle in the original post) to flank that part of the query with double quotes.

and @Jesse kindly reminded me that it would be more focused to write:

"project [201217] Some Project Name"

so the projectFoundOrCreated function now adopts that pattern too in version 3.

2 Likes

Yes! You nailed it!

Thnx again for making this script for me! I really appreciate it! I also saw you have already added this script to the wiki :slight_smile:

== Feek

I think we owe that to the quiet and generous actions of an editor – I was as pleased to find it there as you were : -)

Hi @complexpoint,

Really thx for the script! Works great.

I’ve one more request. Is it possible to get focus on the project in the target file like ⌘-P (Menu > Palette > Go to Anything) . Because for example when in the target file focus is on a project, and you add / copy an item, you have no focus on that specific project.

== Feek

Ver 6 (updated behind the disclosure triangle in the original posting above)

now leaves the target document in the foreground, and narrows its focus to the target project.

( Let me know if that’s not quite what you are after )

1 Like

Yess!! Exactly as I wanted :slight_smile:

And of course the best wishes for 2021!!

== Feek

1 Like

Thanks ! and a good year to you and yours, as well : -)