An update on my habit tracker and a request to make a JavaScript

I have been using TaskPaper to track and improve some desired habits.

Thus far, it is going well most weeks, with noticeable improvements over time.
Here is a copy of my current project:

— Performance Tracker → 1/9 — 1/15:
	- Practice Colemak-DHm → ●●●●●●●
	- Drink 40oz of water → ●●●●●●○
	- Walk thirty minutes → ●●●●●●
	- Intermittent fasting → ●●●●●●++
	- Brainstorm → ●●○○○
	- Watch → ●○○○○
	- Read → ●○○○○
	- Write → ○○○

I’ve changed the name of the project from Habit Tracker to Performance Tracker. I have found that this change is surprisingly inspirational. Words are powerful to me. Finding the right one is important, and it has psychological benefits. Just thinking out loud here.

Using the hollow dots (○) to visually indicate my goals for the week, and then “filling” them in (●) as a goal is achieved, motivates me. The streak of ●’s is great to look at, like a mini-Seinfeld “Don’t Break The Chain” system.

If I exceed my weekly goals (such as doing an extra day or a longer period), I add a plus (+) to record my success. For whatever reason, this technique makes me smile.

The only issue, or friction, at the moment, is how many steps it takes to change the ○ to a ●, or add a + to the end of the line.

So, my proposed solution is to create the following JavaScript. Since this is venturing into new JavaScript territory for me, if Robin, Jesse or anyone else can help, it will be truly appreciated.

Here is what I want the JavaScript to do:

  • With the text cursor already someplace in one of this project’s tasks…
    - When I trigger the script, the first instance of ○ is replaced with a ●
    - If no ○’s remain, a + is placed at the end of the line

I believe that the above will remove friction from the use of my system (as it will remove many steps that currently slow me down), and it will push me to perform better throughout my life.

Any thoughts on the above are welcome. Although my system has proven itself to be useful, there is always room for improvement.

Here’s a couple of spare parts to get you started:

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

    // ------------- INCREMENTED STEP STRING -------------

    // nextStep :: String -> String
    const nextStep = s => {
        const
            parts = group([...s]).map(x => x.join("")),
            nParts = parts.length;

        return 1 > nParts ? (
            "+"
        ) : 2 > nParts ? (
            "○" === parts[0][0] ? (
                `●${parts[0].slice(1)}`
            ) : `${parts[0]}+`
        ) : "○" === parts[1][0] ? (
            `${parts[0]}●${parts[1].slice(1)}`
        ) : `${s}+`;
    };

    // ---------------------- TEST -----------------------

    // main :: IO ()
    const main = () => {
        const arrowGap = " → ";

        return [
            "Brainstorm → ",
            "Brainstorm → ●●●●",
            "Brainstorm → ○○○",
            "Brainstorm → ●●○○○",
            "Brainstorm → ●●●●+"
        ].map(
            s => second(nextStep)(
                s.split(arrowGap)
            ).join(arrowGap)
        ).join("\n");
    };


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

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        // A pair of values, possibly of
        // different types.
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });

    // group :: [a] -> [[a]]
    const group = xs =>
        // A list of lists, each containing only
        // elements equal under (===), such that the
        // concatenation of these lists is xs.
        groupBy(a => b => a === b)(xs);


    // groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
    const groupBy = eqOp =>
        // A list of lists, each containing only elements
        // equal under the given equality operator,
        // such that the concatenation of these lists is xs.
        xs => 0 < xs.length ? (() => {
            const [h, ...t] = xs;
            const [groups, g] = t.reduce(
                ([gs, a], x) => eqOp(x)(a[0]) ? (
                    Tuple(gs)([...a, x])
                ) : Tuple([...gs, a])([x]),
                Tuple([])([h])
            );

            return [...groups, g];
        })() : [];


    // second :: (a -> b) -> ((c, a) -> (c, b))
    const second = f =>
        // A function over a simple value lifted
        // to a function over a tuple.
        // f (a, b) -> (a, f(b))
        ([x, y]) => [x, f(y)];

    // ---
    return main();
})();

Thank you @complexpoint !

This will be fascinating to complete!

1 Like

Let us know if you get stuck, or puzzlements arise.

Thanks @complexpoint !

So, the first part that I am puzzled by is the use of Brainstorm in your code. My intention is that this script will work on whichever line the text cursor is currently in. That way, I can move the text cursor to the item that I want to update, and trigger the script. If the script looks for Brainstorm, wouldn’t be limited to working just on the tine that contains the word brainstorm?

“Brainstorm” is just a string which happens to be in the test samples.

That --- TEST --- section is just checking the relationship between inputs and outputs of the nextStep function.

I’ve left the IO (TaskPaper selection capture and text updating) to you : -)

Thanks!

It looks like this one will being challenging.

Does this mean that:

  • when a particular project is selected (by the presence of a cursor in one of its lines),
  • all of its tasks get a nextStep update
    ?

Or is the scheme to update only the selected task ?

The scheme is to update only the selected task.

So you’re going to obtain:

editor.selection.startItem

and then update its .bodyContentString ?

function TaskPaperContext(editor, options) {
  const outline = editor.outline
    let currentItem = editor.selection.startItem
    if (!currentItem) {
        return
    }

I’ve started with the above. Am I close to a good start?

That looks fine.

A couple of thoughts:

  • are we going to need a reference to outline ? (You might be able to drop that)
  • you’re switching from const to let for currentItem ? (you would only need to do that if you were going to use the same name for some other value later, which is probably not what you’re planning, and tends to risk complications)

The core will probably have the shape of:

currentItem.bodyContentString = (
    updated(currentItem.bodyContentString)
)

So perhaps, roughly, without checking that an eligible line is selected:

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

    // eslint-disable-next-line max-lines-per-function
    const TaskPaperContext = editor => {

        const tp3Main = () => {
            const currentItem = editor.selection.startItem;

            return Boolean(currentItem) ? (
                // Unimplemented :: capture selection position
                // ..... ,
                currentItem.bodyContentString = (
                    updated(currentItem.bodyContentString)
                )
                // ,
                // Unimplemented :: restore selection position
                // .....
                // Return a value if testing:
                // currentItem.bodyContentString
            ) : "Nothing selected in TaskPaper";
        };

        // updated :: String -> String
        const updated = s => {
            const arrowGap = " → ";

            return s.includes(arrowGap) ? (
                second(nextStep)(
                    s.split(arrowGap)
                ).join(arrowGap)
            ) : s;
        };

        // nextStep :: String -> String
        const nextStep = s => {
            const
                parts = group([...s]).map(x => x.join("")),
                nParts = parts.length;

            return 1 > nParts ? (
                "+"
            ) : 2 > nParts ? (
                "○" === parts[0][0] ? (
                    `●${parts[0].slice(1)}`
                ) : `${parts[0]}+`
            ) : "○" === parts[1][0] ? (
                `${parts[0]}●${parts[1].slice(1)}`
            ) : `${s}+`;
        };

        // ------- GENERICS FOR TASKPAPER CONTEXT --------

        // Tuple (,) :: a -> b -> (a, b)
        const Tuple = a =>
            // A pair of values, possibly of
            // different types.
            b => ({
                type: "Tuple",
                "0": a,
                "1": b,
                length: 2,
                *[Symbol.iterator]() {
                    for (const k in this) {
                        if (!isNaN(k)) {
                            yield this[k];
                        }
                    }
                }
            });

        // group :: [a] -> [[a]]
        const group = xs =>
            // A list of lists, each containing only
            // elements equal under (===), such that the
            // concatenation of these lists is xs.
            groupBy(a => b => a === b)(xs);

        // groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
        const groupBy = eqOp =>
            // A list of lists, each containing only elements
            // equal under the given equality operator,
            // such that the concatenation of these lists is xs.
            xs => 0 < xs.length ? (() => {
                const [h, ...t] = xs;
                const [groups, g] = t.reduce(
                    ([gs, a], x) => eqOp(x)(a[0]) ? (
                        Tuple(gs)([...a, x])
                    ) : Tuple([...gs, a])([x]),
                    Tuple([])([h])
                );

                return [...groups, g];
            })() : [];

        // second :: (a -> b) -> ((c, a) -> (c, b))
        const second = f =>
            // A function over a simple value lifted
            // to a function over a tuple.
            // f (a, b) -> (a, f(b))
            ([x, y]) => [x, f(y)];

        return tp3Main();
    };


    const jxaMain = () => {
        const docs = Application("TaskPaper").documents;

        return 0 < docs.length ? (
            docs.at(0).evaluate({
                script: `${TaskPaperContext}`
            })
        ) : "No documents open in TaskPaper";
    };

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


    return jxaMain();
})();

but you might also want to:

  1. Capture the position of the selection before the update, and
  2. and restore it afterwards.

The restoration might look something like:

Expand disclosure triangle to view JS Source
editor.moveSelectionToRange(
    startLocn,
    editor.getLocationForItemOffset(
        endItem, endOffset
    )
)

More generally, for fewer dependencies, you could alternatively try some regex-fiddling to count the numbers of:

  • +

characters and then reassemble a string with one of the lengths incremented, using JS String.repeat(...)

For example, breaking it up into three simple patterns rather than one groupfest:

Expand disclosure triangle to view JS Source
// nextStep2 :: String -> String
const nextStep2 = s => {
    const succScore = ([done, due, extra]) =>
        0 < due ? (
            [done + 1, due - 1, 0]
        ) : [done, due, extra + 1];

    const [done, due, plus] = succScore([
            s.match(/●+/gu),
            s.match(/○+/gu),
            s.match(/\++$/gu)
        ]
        .map(
            v => null !== v ? (
                v[0].length
            ) : 0
        )
    );

    return `${"●".repeat(done)}${"○".repeat(due)}${"+".repeat(plus)}`;
};

My work schedule has been changed to a three week cycle, which I hate. My old schedule was M-F, with the weekends off. Now, my schedule varies week to week.

I had to work this weekend, and I have today off. Having Monday off feels all kinds of wrong. So I have been out of sorts for the last week, and I apologize for not responding sooner.

Thank you, from the newbie.

Another newbie mistake. I know better, and should be using const.

Ok. I am going to examine the code that you have posted and see if I can integrate the parts.

Thank you @complexpoint !

Ok. I am trying to get the restoration part working first.

I think this is capturing the position of the selection:

const startLocn = editor.selection.location;

I am stuck on this attempt:

(() => {
    "use strict";

    // eslint-disable-next-line max-lines-per-function
    const TaskPaperContext = editor => {

        const tp3Main = () => {
            const currentItem = editor.selection.startItem;
            const startLocn = editor.selection.location;

            return Boolean(currentItem) ? (
                // Unimplemented :: capture selection position
                // ..... ,
                currentItem.bodyContentString = (
                    updated(currentItem.bodyContentString)
                )
                // ,
                // Unimplemented :: restore selection position
                // .....
                // Return a value if testing:
                // currentItem.bodyContentString
            ) : "Nothing selected in TaskPaper";
        };

        // updated :: String -> String
        const updated = s => {
            const arrowGap = " → ";

            return s.includes(arrowGap) ? (
                second(nextStep)(
                    s.split(arrowGap)
                ).join(arrowGap)
            ) : s;
        };

        // nextStep :: String -> String
        const nextStep = s => {
            const
                parts = group([...s]).map(x => x.join("")),
                nParts = parts.length;

            return 1 > nParts ? (
                "+"
            ) : 2 > nParts ? (
                "○" === parts[0][0] ? (
                    `●${parts[0].slice(1)}`
                ) : `${parts[0]}+`
            ) : "○" === parts[1][0] ? (
                `${parts[0]}●${parts[1].slice(1)}`
            ) : `${s}+`;
        };

        editor.moveSelectionToRange(
            startLocn,
            editor.getLocationForItemOffset(
                endItem, endOffset
            )
        )

        // ------- GENERICS FOR TASKPAPER CONTEXT --------

        // Tuple (,) :: a -> b -> (a, b)
        const Tuple = a =>
            // A pair of values, possibly of
            // different types.
            b => ({
                type: "Tuple",
                "0": a,
                "1": b,
                length: 2,
                *[Symbol.iterator]() {
                    for (const k in this) {
                        if (!isNaN(k)) {
                            yield this[k];
                        }
                    }
                }
            });

        // group :: [a] -> [[a]]
        const group = xs =>
            // A list of lists, each containing only
            // elements equal under (===), such that the
            // concatenation of these lists is xs.
            groupBy(a => b => a === b)(xs);

        // groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
        const groupBy = eqOp =>
            // A list of lists, each containing only elements
            // equal under the given equality operator,
            // such that the concatenation of these lists is xs.
            xs => 0 < xs.length ? (() => {
                const [h, ...t] = xs;
                const [groups, g] = t.reduce(
                    ([gs, a], x) => eqOp(x)(a[0]) ? (
                        Tuple(gs)([...a, x])
                    ) : Tuple([...gs, a])([x]),
                    Tuple([])([h])
                );

                return [...groups, g];
            })() : [];

        // second :: (a -> b) -> ((c, a) -> (c, b))
        const second = f =>
            // A function over a simple value lifted
            // to a function over a tuple.
            // f (a, b) -> (a, f(b))
            ([x, y]) => [x, f(y)];

        return tp3Main();
    };


    const jxaMain = () => {
        const docs = Application("TaskPaper").documents;

        return 0 < docs.length ? (
            docs.at(0).evaluate({
                script: `${TaskPaperContext}`
            })
        ) : "No documents open in TaskPaper";
    };

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


    return jxaMain();
})();

Have you bound values to those names ?


::moveSelectionToRange(focusLocation, anchorLocation?)

Set selection by character locations in text buffer.

Argument Description
focusLocation Selection focus character Number location.
anchorLocation? Selection anchor character Number location.

Am I moving in the right direction with:

		const
            currentItem = editor.selection.startItem,
            startLocn = currentItem.location,
            endItem = currentItem.endItem,
            endOffset = currentItem.endOffset;

Tell me more about this one:

currentItem.endItem

?

( I don’t think that Item has an .endItem property, though selection does )

Item · Reference


(On my way out of town, this morning, but I’ll take a look this evening, if it’s still looking intractable, and no one has beaten me to it : -)

1 Like

Something like this ?

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

    // eslint-disable-next-line max-lines-per-function
    const TaskPaperContext = editor => {

        const tp3Main = () => {
            const
                selection = editor.selection,
                currentItem = selection.startItem;

            return Boolean(currentItem) ? (() => {
                const
                    startLocn = selection.start,
                    endLocn = selection.end;

                return (
                    // Effects
                    currentItem.bodyContentString = (
                        updated(
                            currentItem.bodyContentString
                        )
                    ),
                    editor.moveSelectionToRange(
                        startLocn,
                        endLocn
                    ),
                    // Value
                    currentItem.bodyContentString
                );
            })() : "Nothing selected in TaskPaper";
        };

        // updated :: String -> String
        const updated = s => {
            const arrowGap = " → ";

            return s.includes(arrowGap) ? (
                second(nextStep)(
                    s.split(arrowGap)
                ).join(arrowGap)
            ) : s;
        };

        // nextStep :: String -> String
        const nextStep = s => {
            const succScore = ([done, due, extra]) =>
                0 < due ? (
                    [done + 1, due - 1, 0]
                ) : [done, due, extra + 1];

            const [done, due, plus] = succScore([
                    s.match(/●+/gu),
                    s.match(/○+/gu),
                    s.match(/\++$/gu)
                ]
                .map(
                    v => null !== v ? (
                        v[0].length
                    ) : 0
                )
            );

            return `${"●".repeat(done)}${"○".repeat(due)}${"+".repeat(plus)}`;
        };

        // ------- GENERICS FOR TASKPAPER CONTEXT --------

        // second :: (a -> b) -> ((c, a) -> (c, b))
        const second = f =>
            // A function over a simple value lifted
            // to a function over a tuple.
            // f (a, b) -> (a, f(b))
            ([x, y]) => [x, f(y)];

        return tp3Main();
    };


    const jxaMain = () => {
        const docs = Application("TaskPaper").documents;

        return 0 < docs.length ? (
            docs.at(0).evaluate({
                script: `${TaskPaperContext}`
            })
        ) : "No documents open in TaskPaper";
    };

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


    return jxaMain();
})();
2 Likes

Thank you @complexpoint — initial tests show that your code nailed it.

After work, I will come back to this and test some more.

And I need to ask some JavaScript questions. I think I may have figured out one of the challenges I have when reading and writing JavaScript code. I think that I am making bad assumptions, due to my familiarity with AppleScript. More later.

1 Like