Tinderbox Forum

Looking for way to set alarm to ToDo (list) / external app?

Taking notes entails getting some alert for some ToDo.
I realise there’s a way to share / sync with Apple Notes, and even that Apple Notes has folder support.

1 - Where can I find example usage or more in depth information on how to interact with watching / sharing Apple Notes?
2 - What workaround do you use to attach a timely alarm to a note?

Though not my area of expertise, I’d have thought this is somewhere where the new AppleScript/OSAscript support in v8+ ought to help. if Tinderbox calls out via AppleScript you can interact with all sorts of other apps.

@ComplexPoint might have a more informed view on that.

I personally set alerts with Due.app – you can see the basic script mechanism in the thread linked below:

Interaction with Notes or Reminders would also be possible. Can you show us a sample of:

  • The notes and attributes on which you would like to base alerts
  • a sample of the kind of alert that you would like to automate.

Not sure that I have quite grasped the connection between setting alarms and your reference to the Notes app, but again showing is always more eloquent than telling – perhaps you could show us an example ?

In the meanwhile, first a barebones sketch of using AppleScript to create a Reminder with a dated alert (and a link back to a Tinderbox 8 note) in a specified list in the macOS Reminders.app

(From a selected note in TBX8)

(Could be run from Script Editor, or more easily from something like Keyboard Maestro)

use AppleScript version "2.4"
use framework "Foundation"
use framework "JavaScriptCore"
use scripting additions

-- Rob Trew 2019
-- Ver 0.02

tell application "Tinderbox 8"
    
    -- EDIT THESE TO MATCH A TINDERBOX ATTRIBUTE NAME    
    set dateAttributeName to "StartDate"
    
    -- AND AN EXISTING REMINDERS LIST NAME ON YOUR SYSTEM
    set RemindersListName to "General"
    
    set maybeNote to selected note of front document
    if missing value is not maybeNote then
        tell maybeNote
            set strName to value of attribute "Name"
            set strDate to value of attribute dateAttributeName
            set strURL to value of attribute "NoteURL"
        end tell
        
        -- DIALLING OUT TO JAVASCRIPT FOR HELP WITH PARSING THE 
        -- ISO8601 DATE STRING RETURNED FROM TINDERBOX 8
        set jsc to current application's JSContext's new()
        set asDate to ((jsc's evaluateScript:("new Date('" & strDate & "')"))'s ¬
            toObject() as date)
        set jsc to missing value
        
        tell application "Reminders"
            tell (first list whose name = RemindersListName)
                set oAlert to make new reminder with properties ¬
                    {name:strName, body:strURL, remind me date:asDate}
            end tell
        end tell
    else
        "No note selected in TinderBox 8"
    end if
end tell
2 Likes

and then a fuller JavaScript for Automation Script, also for use either from Script Editor (with the language tab at top left set to JavaScript) or from Keyboard Maestro etc.

This version provides alerts and richer explanatory messages if it is run for example, when any of the following turn out to be the case:

  • Tinderbox 8 is not running
  • No document is open in TB8
  • Nothing is selected in TB8
  • The named attribute doesn’t exist (as spelled) in the front Tinderbox document
  • The named attribute value doesn’t contain something which Tinderbox can parse as a date
  • Reminders doesn’t contain a list matching the list name provided

When a reminder with an alert is created, it shows a dialog of this kind:

and creates an alert in the specified Reminders list like:

(() => {
    'use strict';

    // Rob Trew  2019
    // Ver 0.06

    // Added explanatory messages for case where the string
    // given for alertDateAttribName doesn't match an
    // attribute in the front Tinderbox 8 document.

    // alertDateAttribName, remindersListName :: String
    const
        alertDateAttribName = 'StartDate',
        remindersListName = 'General';

    // main :: IO ()
    const main = () =>
        either(
            alert('Could not create alert'), // + explanatory message.
            x => (
                Application('Reminders').activate(),
                alert('Reminder alert created')(x)
            ),
            bindLR(
                tbxTitle_URL_DateFromSelectedNoteLR(
                    alertDateAttribName
                ),
                reminderAlertFromNameAndDateLR(
                    remindersListName
                )
            )
        );


    // TINDERBOX FUNCTIONS --------------------------------

    // tbxAttribValLR :: TBX Note -> String -> String
    const tbxAttribValLR = note => k => {
        // Either an explanatory message (Left) or
        // an attribute value (Right).
        const mb = note.attributes.byName(k);
        return mb.exists() ? (
            Right(mb.value())
        ) : Left('Attribute not found: ' + k);
    };

    // tbxFrontDocLR :: Tbx IO () -> Either String Tbx Doc
    const tbxFrontDocLR = () => {
        // Either an explanatory message if no documents are open,
        // or or Tinderbox 8 is not running,
        // or a reference to the front Tinderbox 8 document.
        const tbx = Application('Tinderbox 8');
        return tbx.running() ? (() => {
            const ds = tbx.documents;
            return 0 < ds.length ? (
                Right(ds.at(0))
            ) : Left('No documents open in Tinderbox');
        })() : Left('Tinderbox 8 is not running.');
    };

    // tbxSelectedNoteLR :: Tbx Doc -> Either String Tbx Note
    const tbxSelectedNoteLR = doc => {
        // Either an explanatory message if nothing is selected,
        // or a reference to the first selected note.
        const note = doc.selectedNote();
        return note !== null ? (
            Right(note)
        ) : Left('No note selected in ' + doc.name());
    };

    // tbxTitle_URL_DateFromSelectedNoteLR ::
    //      String -> Either String (String, URL, Date)
    const tbxTitle_URL_DateFromSelectedNoteLR = attributeName =>
        // Either an explanatory message if no valid date is found,
        // or a (name, url, date) triple.
        bindLR(
            bindLR(
                tbxFrontDocLR(),
                tbxSelectedNoteLR
            ),
            note => {
                // attribValLR :: String -> String
                const attribValLR = tbxAttribValLR(note);
                return bindLR(
                    attribValLR(attributeName),
                    s => {
                        const maybeDate = new Date(s);
                        return isNaN(maybeDate) ? (
                            Left('Not a valid date: "' + s + '"')
                        ) : Right(Tuple3(
                            attribValLR('Name').Right,
                            attribValLR('NoteURL').Right,
                            maybeDate
                        ))
                    }
                );
            }
        );


    // JXA AND REMINDERS ----------------------------------

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

    // reminderAlertFromNameAndDate :: String -> (String, URL,  Date) ->
    //  -> Either String (IO macOS reminder creation)
    const reminderAlertFromNameAndDateLR = remindersListName =>
        tupleNameURLDate => {
            const
                appRems = Application('Reminders'),
                listsWithMatchingName = appRems.lists.where({
                    name: remindersListName
                });
            return 0 < listsWithMatchingName.length ? (() => {
                const [title, url, alertDate] = Array.from(
                    tupleNameURLDate
                );
                return (
                    appRems.lists.byName(remindersListName)
                    .reminders.push(appRems.Reminder({
                        name: title,
                        body: url,
                        remindMeDate: alertDate
                    })),
                    Right(
                        'New alert created in ' +
                        remindersListName + ' list:\n\n"' + title +
                        '"\n\n' + alertDate
                    )
                );
            })() : Left(
                'Reminders list not found: "' +
                remindersListName + '".\n\n' + 'Lists found:\n' +
                bulleted('    ', unlines(appRems.lists.name()))
            );
        };


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

    // Tuple3 (,,) :: a -> b -> c -> (a, b, c)
    const Tuple3 = (a, b, c) => ({
        type: 'Tuple3',
        '0': a,
        '1': b,
        '2': c,
        length: 3
    });

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // bulleted :: String -> String -> String
    const bulleted = (strIndent, s) =>
        s.split(/[\r\n]/).map(
            x => '' !== x ? strIndent + '- ' + x : x
        ).join('\n')

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

    // showLog :: a -> IO ()
    const showLog = (...args) =>
        console.log(
            args
            .map(JSON.stringify)
            .join(' -> ')
        );

    // unlines :: [String] -> String
    const unlines = xs => xs.join('\n');

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

Wow, thanks a lot!
I’ll need some time to digest :wink:
Will post back when I have feedback available.

(I’m using Due on iOS but was not aware it’s available for macOS too)

1 Like

Great to see these examples! On the AppleScript one, it’s really cool to see how to “dial out” to JavaScript. Of course, lowly AppleScript can handle the task itself without much trouble, with a handler (subroutine/function) something like this:

set strDate to "2019-06-02T11:41:15+08:00" -- a Tinderbox date-time looks like this

set appleScriptDate to convertToAsDate(strDate)

to convertToAsDate(isoDate)
	set text item delimiters to {"-", "T", "+"}
	set asDate to current date -- a "placeholder", with actual values substituted in below
	tell isoDate's text items to set {asDate's year, asDate's month, asDate's day} to {item 1, item 2, item 3}
	tell isoDate's text item 4 to set asDate's time to hours * (word 1) + minutes * ((word 2) + (word 3) / 60)
	return asDate
end convertToAsDate
1 Like

The part that I personally prefer to leave to the authors of industrially tested DateTime libraries is the question of time zones, summer time etc etc (all beyond my age and pay-grade)

(and if TB is ever used on Mars, or g-d forbid, further afield, I think I may, at that point, also prefer to delegate any minor relativistic adjustments to someone else’s DateTime library :slight_smile: )

On the subject of time zones and their discontents, two Tinderbox bugs that actually occurred in everyday use are worth remembering:

  1. A historian was working with on some notes about notes about events in the 19th-century, and using an action to calculate the interval between notes. At some point, it was clear that the interval of two notes in 1832 or 1833 was off by about half a day. Why? In 1833, the historian was working in the US, and US introduced time zones in that year. One note was after time zones were introduced, and so Tinderbox treated it as EST. The other, a few days before, was pre-time-zone: Tinderbox treated it as GMT.

  2. A journalist, who was traveling a great deal, kept Tinderbox open on his laptop. Tinderbox tended to crash immediately upon arrival, but only after a long flight. Why? Because one of the things your computer does when you arrive someplace is to check where you are and what the time is. Tinderbox checks out a special calendar object at startup for doing date calculations; this is a fairly expensive object to create and so we keep it around instead of making a new one whenever we need it. However, that object turns into a pumpkin if you change time zones.

As you can imagine, each of these was a bear to fix!

3 Likes

an alternative, if you prefer, understandably, to avoid firing up an instance of JSContext from AppleScript (but would still like a tested date library to handle the edge cases) is to:

  • add the incantation "use framework Foundation" at the top of your script,
  • and study the ObjC (rather than Swift) documentation of NSISO8601DateFormatter

https://developer.apple.com/documentation/foundation/nsiso8601dateformatter

Here’s a first sketch, but it might be good to test a bit and read up the details, just in case I’ve missed anything.

(remember of course, that once we use the ObjC interface, we have to be careful to clear any C pointer references before the script ends, either simply with end tell , or by resetting any variables pointing to ObjC interface values to missing value

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions


-- https://developer.apple.com/documentation/foundation/nsiso8601dateformatter

-- dateFromTbxString :: String -> Date
on dateFromTbxString(isoDateString)
    if "never" ≠ isoDateString then
        set ca to current application
        tell ca's NSISO8601DateFormatter's alloc's init()
            set its formatOptions to (ca's NSISO8601DateFormatWithInternetDateTime as integer)
            set dteParsed to (its dateFromString:(isoDateString)) as date
        end tell
        return dteParsed
    else
        return missing value
    end if
end dateFromTbxString


-- TEST -----------------------------------------------------------
-- Assuming a selected note with a date value other than 'never' in the StartDate attribute
on run
    tell application "Tinderbox 8"
        tell selected note of front document to set strDate to value of attribute "StartDate"
    end tell
    
    return dateFromTbxString(strDate)
end run

I blame time zones for iPhone alarms that failed to go off and for “wrong” times in Calendar. I can see how they can bedevil developers.

I took the three approaches (“dialing out” to JavaScript, NSISO8601DateFormatter, and good old basic AppleScript string manipulation) and passed to each the string “2019-06-02T23:41:16+08:00”.

"dialing out" to JavaScript (click to reveal)
use AppleScript version "2.4"
use framework "Foundation"
use framework "JavaScriptCore"
use scripting additions

to dateFromTbxString(isoDateString)
	set jsc to current application's JSContext's new()
	set asDate to ((jsc's evaluateScript:("new Date('" & isoDateString & "')"))'s toObject() as date)
	set jsc to missing value
	return asDate
end dateFromTbxString

-- TEST -----------------------------------------------------------

set strDate to "2019-06-02T23:41:16+08:00"
set asDate to dateFromTbxString(strDate)
return {strDate, asDate}
NSIS08601DateFormatter
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

-- dateFromTbxString :: String -> Date
on dateFromTbxString(isoDateString)
	set ca to current application
	tell ca's NSISO8601DateFormatter's alloc's init()
		set its formatOptions to (ca's NSISO8601DateFormatWithInternetDateTime as integer)
		set dteParsed to (its dateFromString:(isoDateString)) as date
	end tell
	return dteParsed
end dateFromTbxString


-- TEST -----------------------------------------------------------

set strDate to "2019-06-02T23:41:16+08:00"
set asDate to dateFromTbxString(strDate)

return {strDate, asDate}
good old basic AppleScript string manipulation
to convertToAsDate(isoDate)
	set text item delimiters to {"-", "T", "+"}
	set asDate to current date -- a "placeholder" date; actual values substituted in below
	tell isoDate's text items to set {asDate's year, asDate's month, asDate's day} to {item 1, item 2, item 3}
	tell isoDate's text item 4 to set asDate's time to hours * (word 1) + minutes * ((word 2) + (word 3) / 60)
	return asDate
end convertToAsDate

-- TEST -----------------------------------------------------------

set strDate to "2019-06-02T23:41:16+08:00"
set asDate to convertToAsDate(strDate)

return {strDate, asDate}

They each returned the AppleScript date “Sunday, June 2, 2019 at 11:41:16 PM”. (AppleScript dates have no notion of time zone).

Then I passed them the string “2019-06-02T11:41:16+07:00”.

The first two now returned the AppleScript date “Monday, June 3, 2019 at 12:41:16 AM”. The third, the good old basic AppleScript string manipulation, as expected still returned “Sunday, June 2, 2019 at 11:41:16 PM”.

Results will differ, of course, on machines with different Date & Time settings.

I think I would typically want to get the date and time stored in Tinderbox into another app via AppleScript without any hidden automatic adjustments for time zone, then (only if relevant) make any time zone adjustments explicitly. For that the good old basic AppleScript string manipulation approach works best for those receiving apps (presumably including Reminders?) that can only accept AppleScript dates from AppleScript. Plus it’s shorter and easier.

But it’s great to have the option to go the other way too!

Good to see all three in test :slight_smile:

Not sure if you are a region which practices Summer Time adjustment, but:

one issue that some of use have bear in mind (even at times when the city of date-making will tend to be the city of date-consumption, and there is little danger of aeroplanes in sight) is that a date created on one side of a (spring forward, fall back) Summer Time adjustment may be consumed on the other side of that adjustment.

(and near such boundaries, of course, any calculations of duration need be carefully thought through to avoid unintended results)

The system’s underlying classes, NSDate and NSCalender, do an extraordinary job of dealing with daylight savings time and related complexities. FWIW

1 Like

11 posts were split to a new topic: Tinderbox Date data and timezone offsets