Tinderbox Forum

Script: Timed Due.app 'sprints' for selected notes


(RobTrew) #1

In the tradition of plastic kitchen tomatoes and focused ‘sprints’ of c. 25 mins on component sub-projects:

I personally use Due.app for these timed sprints, mainly because I find that its classical guitar sound makes a familiar and fairly gentle recall from narrow focus to broader context.

Due.app reminders can

  1. be set for N mins hence
  2. contain a text which contains a hyperlink (launched with the → key)
  3. be initiated from outside through a url scheme

The link in one reminder can be a url for a following reminder, so we can automatically set up a chain of 3 or 4 sprints if we want.

This script (osascript JS - JavaScript for Automation - JXA) which I personally launch from and customise with Keyboard Maestro:

  1. Captures the names of any notes that are selected in the Tinderbox 8 GUI
  2. Optionally captures any integers from a custom attribute, perhaps with a name like mins, which you can specify, as an indication of the sprint length wanted.
  3. Constructs a nested Due.app url

Once the first sprint has been launched by the url, you can tap → in Due to start the next one. And so forth for each note-sprint encoded in the nest.

So, for example, from a three-note selection like:

The script would first launch Due.app with this sprint, which itself contains a further url-encoded sprint:

From which the next sprint-link could later be launched (at or after the sound of the first reminder sound) with the → key, or from a gear-wheel menu:

leading to:

The default sprint length, and the name of any custom duration attribute to use in Tinderbox, can be adjusted in the Keyboard Maestro macro, or by editing the script directly.

Keyboard Maestro macro at:

JavaScript source

(() => {
    'use strict';

    // Rob Trew 2019 
    // Ver 0.01

    // SETTINGS -------------------------------------------
    const
        kme = Application('Keyboard Maestro Engine'),

        // The name of an (optional) attribute, with an
        // integer value representing a number of minutes.
        k = kme.getvariable('minsAttributeName') || 'mins',

        // A default number of minutes for a plastic tomato
        // dash or sprint of attention to a note or issue.
        defaultMins = kme.getvariable('minsDefault') || 25;


    // ----------------------------------------------------
    // Due.app URL, with text and duration in minutes,
    // for any selected Tinderbox 8 items

    // main :: IO ()
    const main = () => {
        const
            tbx8 = Application('Tinderbox 8'),
            ds = tbx8.documents;

        return bindLR(
            bindLR(
                0 < ds.length ? (
                    Right(ds.at(0))
                ) : Left('No documents open in Tinderbox 8'),
                d => {
                    const xs = d.selections();
                    return 0 < xs.length ? Right(
                        concatMap(
                            x => {
                                const
                                    attr = x.attributes.byName(k),
                                    txt = x.name().trim();
                                return 0 < txt.length ? [Tuple(
                                    txt,
                                    attr.exists() ? (
                                        parseInt(attr.value())
                                    ) : (
                                        console.log(
                                            'No "' + k +
                                            '" attribute for: "' + txt +
                                            '". (Using default: ' +
                                            defaultMins.toString() +
                                            ' mins).'
                                        ),
                                        defaultMins
                                    )
                                )] : []
                            },
                            xs
                        )
                    ) : Left('Nothing selected in ' + d.name());
                }
            ),
            nestedDueURL
        );
    };

    // DUE URL --------------------------------------------

    // nestedDueURL :: [(String, Int)] -> URL String
    const nestedDueURL = tms => {
        const go = xs => {
            const
                t = xs.slice(1),
                [s, n] = Array.from(xs[0]);
            return 'due://x-callback-url/add?title=' +
                encodeURIComponent(
                    s + (0 < t.length ? ' '.repeat(12) +
                        go(t) : '')
                ) + '&minslater=' + n.toString();
        };
        return 0 < tms.length ? go(tms) : '';
    };

    // GENERIC FUNCTIONS ----------------------------------
    // https://github.com/RobTrew/prelude-jxa

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

    // concatMap :: (a -> [b]) -> [a] -> [b]
    const concatMap = (f, xs) =>
        xs.reduce((a, x) => a.concat(f(x)), []);

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = (fl, fr, e) =>
        'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

    // id :: a -> a
    const id = x => x;

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


(Mark Anderson) #3

Nice idea. Niche, I grant but for those deep into automation it would be a nice thing if not an implementation nightmare.


(RobTrew) #4

On the supply-side, possibly.

( Less so, I would guess, on the demand side. )


(Sumner Gerard) #6

I was thinking eventually it would be nice to have a image menu similar to DEVONthink but can the Systemwide Script Menu do the job without using developer resources?


(Paul Walters) #7

It can. If you store scripts in a folder you make in ~/Library/Scripts/Applications/…. (And one can use KeyboardMaestro, or BetterTouchTool, or Typinator, or TextExpander, or Alfred, or LaunchBar, etc. etc. to launch scripts.)