How to sink a branch to the top of the bottom done pile

Often times i have tasks that once complete, i like to move to the top of the @done stack which are at the bottom. I call this "sink"ing.

Here’s a demonstration where I want to sink the selected branch: https://i.ibb.co/XsmndK3/image.png
Would be cool to automatically select the branch of where cursor is.

How could I accomplish this with a script? The cursor should stay relative to where it is (start of “do task C”) in the example above.

Any guidance on how to start coding this? I guess if it’s easier it would be fine to just sink it to the absolute bottom of the current hierarchy level (not on top of the @done pile).

Thanks!

Could you give your

  • before
  • after

examples as inline text here ?

Ideally with three backticks ``` before the first line
and three backticks after the last line.

(for plain text formatting)

(Firewalls here don’t permit clicking on unknown urls)

This rough draft (for testing with dummy data) might be similar:

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

    // ROUGH EXPERIMENTAL DRAFT
    // (not for use in production)
    // @done in selected peer group triaged to below.

    // Rob Trew @2021 MIT
    // Ver 0.04
    // Added option to mark selected items as @done
    // before the triage.

    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
    // ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
    // LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
    //  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
    // EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
    // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
    // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
    // ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    // ------------------- JXA CONTEXT -------------------
    const main = () => {
        const windows = Application("TaskPaper").windows;

        return 0 < windows.length ? (
            windows.at(0).document.evaluate({
                script: `${TaskPaperContext}`,
                withOptions: {
                    markSelectionsAsDone: true,
                    includeTime: false
                }
            })
        ) : "No windows open in TaskPaper.";
    };

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

    // eslint-disable-next-line max-lines-per-function
    const TaskPaperContext = (editor, options) => {
        const tpMain = () => {
            const
                outline = editor.outline,
                selection = editor.selection,
                selectionRoot = selection.startItem.parent;

            let stampLog = "";

            // -------- MUTATIONS GROUPED FOR ⌘Z ---------
            outline.groupUndoAndChanges(() => {
                stampLog = optionallyStamped(options)(
                    selection.selectedItems
                );

                // Updated TP3 outline.
                foldTree(
                    updatedPeerOrder(outline)
                )(
                    // from model of done-triaged peers.
                    foldTree(triagedByDone)(
                        pureTreeTP3(selectionRoot)
                    )
                );

                editor.moveSelectionToItems(
                    selectionRoot.firstChild
                );
            });

            return stampLog;
        };

        // ---------------- DONE STAMPING ----------------

        // optionallyStamped :: {
        //      markSelectionsAsDone :: Bool,
        //      includeTime :: Bool
        // } -> [TP Item] -> IO String
        const optionallyStamped = opts =>
            items => opts.markSelectionsAsDone ? (() => {
                const
                    nowStamp = timeIncluded(
                        opts.includeTime
                    )(
                        taskPaperDateString(new Date())
                    );

                return items.flatMap(
                    x => Boolean(x.bodyString) ? (
                        x.setAttribute(
                            "data-done", nowStamp
                        ),
                        [x.bodyString]
                    ) : []
                );
            })() : "";

        // ------------- PARTITIONED ON DONE -------------

        // triagedByDone :: TPNode ->
        // [TPNode] -> Tree TPNode
        const triagedByDone = x =>
            // Todos above, @done below.
            compose(
                Node(x),
                concat,
                partition(
                    t => !t.root.hasAttribute(
                        "data-done"
                    )
                )
            );

        // -------- TP UPDATED FROM SORTABLE TREE --------

        // updatedPeerOrder :: TPOutline ->
        // TPNode -> [TPNode] -> IO Tree TPNode
        const updatedPeerOrder = outline =>
            // updatedPeerOrder(outline)
            // can be used as an argument to foldTree
            // for bottom up mapping of a new multi-level
            // sorted Tree Node to the TaskPaper model.
            tpNode => children => Node(tpNode)((
                children.reduceRight(
                    (a, tree) => {
                        const x = tree.root;

                        return (
                            a.id !== x.id && (
                                outline
                                .insertItemsBefore(x, a)
                            ),
                            x
                        );
                    },
                    tpNode.firstChild
                ),
                tpNode.children
            ));

        // ----------------- DATE STRING -----------------

        // iso8601Local :: Date -> String
        const iso8601Local = dte =>
            new Date(dte - (6E4 * dte.getTimezoneOffset()))
            .toISOString();


        // taskPaperDateString :: Date -> String
        const taskPaperDateString = dte => {
            // A string representation of the given date:
            // yyyy-mm-dd hh:mm (Abbreviated local ISO8601)
            const [d, t] = iso8601Local(dte).split("T");

            return [d, t.slice(0, 5)].join(" ");
        };


        // timeIncluded :: Bool -> String -> String
        const timeIncluded = includeTime =>
            // TaskPaper date string,
            // with or without time component.
            tpFullDate => includeTime ? (
                tpFullDate
            ) : tpFullDate.slice(0, 10);


        // -------------- GENERICS FOR TP3 ---------------

        // Node :: a -> [Tree a] -> Tree a
        const Node = v =>
            // Constructor for a Tree node which connects a
            // value of some kind to a list of zero or
            // more child trees.
            xs => ({
                type: "Node",
                root: v,
                nest: xs || []
            });


        // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
        const compose = (...fs) =>
            // A function defined by the right-to-left
            // composition of all the functions in fs.
            fs.reduce(
                (f, g) => x => f(g(x)),
                x => x
            );


        // concat :: [[a]] -> [a]
        const concat = xs =>
            xs.flat(1);


        // foldTree :: (a -> [b] -> b) -> Tree a -> b
        const foldTree = f => {
            // The catamorphism on trees. A summary
            // value obtained by a depth-first fold.
            const go = tree => f(
                tree.root
            )(
                tree.nest.map(go)
            );

            return go;
        };


        // partition :: (a -> Bool) -> [a] -> ([a], [a])
        const partition = p =>
            // A tuple of two lists - those elements in
            // xs which match p, and those which do not.
            xs => xs.reduce(
                (a, x) => p(x) ? (
                    [a[0].concat(x), a[1]]
                ) : [a[0], a[1].concat(x)],
                [
                    [],
                    []
                ]
            );


        // pureTreeTP3 :: TP3Item  -> Tree TP3Item
        const pureTreeTP3 = item => {
            // A tree in which the .root values are nodes
            // in the TaskPaper native model, and the
            // the .nest (children) lists are simple
            // JS Arrays of trees – directly sortable etc.
            const go = x =>
                Node(x)(
                    x.hasChildren ? (
                        x.children.map(go)
                    ) : []
                );

            return go(item);
        };

        // Taskpaper context main.
        return tpMain();
    };

    // JXA main.
    return main();
})();
1 Like

But perhaps you want the script to:

  • mark selected items as @done (if they aren’t already)
  • and then sink them downwards

?


If so, you can edit the value of the markSelectionsAsDone from false to true in version 0.02 (above)

Thanks for the code! While I go through it, here’s the example code you requested. this shows marking branch B as done and sinking it to the bottom.

// before
awesome idea
	do task B @done(2021-09-29)
		do task B.1 @done(2021-09-29)
		do task B.2 @done(2021-09-29)
	do task C
		do task C.1
	do task D
		do task D.1
		do task D.2
	do task A @done(2021-09-29)

// after
awesome idea
	do task C
		do task C.1
	do task D
		do task D.1
		do task D.2
	do task A @done(2021-09-29)
	do task B @done(2021-09-29)
		do task B.1 @done(2021-09-29)
		do task B.2 @done(2021-09-29)
1 Like

Thanks – I’ve had a chance to look at the .png as well now, and I think perhaps the two examples differ in the order of the sunk items ?

B seems to sink below A in the text version, but rest on top of A in the .png ?

If that alternative doesn’t matter too much, then the draft code might be a reasonable match – it uses the latter of those two patterns – simply partitioning todo ⇄ done but otherwise preserving order, with new @done elements sinking down and landing on top of those that are already there.


( and I suppose another approach might be for the script to add a sort by completion date to the @done lines at the bottom )

In the meanwhile, ver 0.3 (above, behind disclosure triangle) adds a second option – if you want the script to mark selected items as @done (if they aren’t already), you can also choose whether or not to include a time stamp with the date stamp.

i.e. one of the following:

  • @done(2021-09-30 13:23)
  • @done(2021-09-30)

Near the top of the script, options are chosen by editing true ⇄ false in:

withOptions: {
    markSelectionsAsDone: true,
    includeTime: false
}
1 Like

A Keyboard Maestro macro ( Mark Done and Sink)

provisionally assigned to ⌘D and with the options at the top of the JXA code set to:

withOptions: {
    markSelectionsAsDone: true,
    includeTime: true
}

Mark done and sink.kmmacros.zip (4.0 KB)

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

    // ROUGH EXPERIMENTAL DRAFT
    // (not for use in production)
    // @done in selected peer group triaged to below.

    // Rob Trew @2021 MIT
    // Ver 0.04
    // Added option to mark selected items as @done
    // before the triage.

    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
    // ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
    // LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
    //  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
    // EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
    // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
    // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
    // ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    // ------------------- JXA CONTEXT -------------------
    const main = () => {
        const windows = Application("TaskPaper").windows;

        return 0 < windows.length ? (
            windows.at(0).document.evaluate({
                script: `${TaskPaperContext}`,
                withOptions: {
                    markSelectionsAsDone: true,
                    includeTime: true
                }
            })
        ) : "No windows open in TaskPaper.";
    };

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

    // eslint-disable-next-line max-lines-per-function
    const TaskPaperContext = (editor, options) => {
        const tpMain = () => {
            const
                outline = editor.outline,
                selection = editor.selection,
                selectionRoot = selection.startItem.parent;

            let stampLog = "";

            // -------- MUTATIONS GROUPED FOR ⌘Z ---------
            outline.groupUndoAndChanges(() => {
                stampLog = optionallyStamped(options)(
                    selection.selectedItems
                );

                // Updated TP3 outline,
                foldTree(
                    updatedPeerOrder(outline)
                )(
                    // from model of done-triaged peers.
                    foldTree(triagedByDone)(
                        pureTreeTP3(selectionRoot)
                    )
                );

                editor.moveSelectionToItems(
                    selectionRoot.firstChild
                );
            });

            return stampLog;
        };

        // ---------------- DONE STAMPING ----------------

        // optionallyStamped :: {
        //      markSelectionsAsDone :: Bool,
        //      includeTime :: Bool
        // } -> [TP Item] -> IO String
        const optionallyStamped = opts =>
            items => opts.markSelectionsAsDone ? (() => {
                const
                    nowStamp = timeIncluded(
                        opts.includeTime
                    )(
                        taskPaperDateString(new Date())
                    );

                return items.flatMap(
                    x => Boolean(x.bodyString) ? (
                        x.setAttribute(
                            "data-done", nowStamp
                        ),
                        [x.bodyString]
                    ) : []
                );
            })() : "";

        // ------------- PARTITIONED ON DONE -------------

        // triagedByDone :: TPNode ->
        // [TPNode] -> Tree TPNode
        const triagedByDone = x =>
            // Todos above, @done below.
            compose(
                Node(x),
                concat,
                partition(
                    t => !t.root.hasAttribute(
                        "data-done"
                    )
                )
            );

        // -------- TP UPDATED FROM SORTABLE TREE --------

        // updatedPeerOrder :: TPOutline ->
        // TPNode -> [TPNode] -> IO Tree TPNode
        const updatedPeerOrder = outline =>
            // updatedPeerOrder(outline)
            // can be used as an argument to foldTree
            // for bottom up mapping of a new multi-level
            // sorted Tree Node to the TaskPaper model.
            tpNode => children => Node(tpNode)((
                children.reduceRight(
                    (a, tree) => {
                        const x = tree.root;

                        return (
                            a.id !== x.id && (
                                outline
                                .insertItemsBefore(x, a)
                            ),
                            x
                        );
                    },
                    tpNode.firstChild
                ),
                tpNode.children
            ));

        // ----------------- DATE STRING -----------------

        // iso8601Local :: Date -> String
        const iso8601Local = dte =>
            new Date(dte - (6E4 * dte.getTimezoneOffset()))
            .toISOString();


        // taskPaperDateString :: Date -> String
        const taskPaperDateString = dte => {
            // A string representation of the given date:
            // yyyy-mm-dd hh:mm (Abbreviated local ISO8601)
            const [d, t] = iso8601Local(dte).split("T");

            return [d, t.slice(0, 5)].join(" ");
        };


        // timeIncluded :: Bool -> String -> String
        const timeIncluded = includeTime =>
            // TaskPaper date string,
            // with or without time component.
            tpFullDate => includeTime ? (
                tpFullDate
            ) : tpFullDate.slice(0, 10);


        // -------------- GENERICS FOR TP3 ---------------

        // Node :: a -> [Tree a] -> Tree a
        const Node = v =>
            // Constructor for a Tree node which connects a
            // value of some kind to a list of zero or
            // more child trees.
            xs => ({
                type: "Node",
                root: v,
                nest: xs || []
            });


        // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
        const compose = (...fs) =>
            // A function defined by the right-to-left
            // composition of all the functions in fs.
            fs.reduce(
                (f, g) => x => f(g(x)),
                x => x
            );


        // concat :: [[a]] -> [a]
        const concat = xs =>
            xs.flat(1);


        // foldTree :: (a -> [b] -> b) -> Tree a -> b
        const foldTree = f => {
            // The catamorphism on trees. A summary
            // value obtained by a depth-first fold.
            const go = tree => f(
                tree.root
            )(
                tree.nest.map(go)
            );

            return go;
        };


        // partition :: (a -> Bool) -> [a] -> ([a], [a])
        const partition = p =>
            // A tuple of two lists - those elements in
            // xs which match p, and those which do not.
            xs => xs.reduce(
                (a, x) => p(x) ? (
                    [a[0].concat(x), a[1]]
                ) : [a[0], a[1].concat(x)],
                [
                    [],
                    []
                ]
            );


        // pureTreeTP3 :: TP3Item  -> Tree TP3Item
        const pureTreeTP3 = item => {
            // A tree in which the .root values are nodes
            // in the TaskPaper native model, and the
            // the .nest (children) lists are simple
            // JS Arrays of trees – directly sortable etc.
            const go = x =>
                Node(x)(
                    x.hasChildren ? (
                        x.children.map(go)
                    ) : []
                );

            return go(item);
        };

        // Taskpaper context main.
        return tpMain();
    };

    // JXA main.
    return main();
})();
2 Likes

brilliant, thank you!

1 Like

Added to Wiki

3 Likes