JS: make reminder syntax

Using javascript am trying to add a reminder with a due date to a record.

Reading the well documented AppleScript dictionary (selecting Javascript variant from the dropdown) it states this:

make method : Make a new object.
make
new: type : The class of the new object.
[at: location specifier] : The location at which to insert the object.
[withData: any] : The initial data for the object.
[withProperties: Record] : The initial values for properties of the object.
→ specifier

Well that certainly makes the syntax crystal clear. The best part about the documentation is the inclusion of example syntax.

With some experimentation and digging through the discourse I ended up here:

app.make({new: 'reminder', withProperties: {schedule: 'once', alarm: 'alert', alarmString: 'alarm string', dueDate: due, at: r}});

(r is the record)

When running this events in the Script Editor yield:

app.Reminder({"schedule":"once", "alarm":"alert", "alarmString":"alarm string", "dueDate":{"$L":"en", "$d":Fri Jul 10 2020 00:00:00 GMT-0400 (EDT), "$x":[object Object], "$y":2020, "$M":6, "$D":10, "$W":5, "$H":0, "$m":0, "$s":0, "$ms":0, "parse":[object Object], "init":[object Object], "$utils":[object Object], "isValid":[object Object], "isSame":[object Object], "isAfter":[object Object], "isBefore":[object Object], "$g":[object Object], "unix":[object Object], "valueOf":[object Object], "startOf":[object Object], "endOf":[object Object], "$set":[object Object], "set":[object Object], "get":[object Object], "add":[object Object], "subtract":[object Object], "format":[object Object], "utcOffset":[object Object], "diff":[object Object], "daysInMonth":[object Object], "$locale":[object Object], "locale":[object Object], "clone":[object Object], "toDate":[object Object], "toJSON":[object Object], "toISOString":[object Object], "toString":[object Object], "millisecond":[object Object], "second":[object Object], "minute":[object Object], "hour":[object Object], "day":[object Object], "month":[object Object], "year":[object Object], "date":[object Object]}, "at":app.databases.byId(2).contents.byId(25447)}).make()

It does seem like we’re on the right track - but we end up with:

Result:
Error -10024: Can’t make or move that element into that container.

Any ideas on how to tweak the syntax much appreciated.

A short trip to the forum’s search function led me to an example in AppleScript. From it, I’d guess that you call make as a method of the record, not the application.

The documentation is what it is because it is generated automatically, I think. That guarantees the uniform formatting and the correctness. The lack of examples is the same for every app there.

I haven’t yet made much use of reminders in DEVONthink, but they seem interesting.

For a quick workaround, just in case testing for compliance with Apple’s OSA scripting interface still has rough edges in DEVONthink (their slightly eccentric use of AppleScript phrasing in the SDEF is probably symptomatic :-), I notice that:

If any existing record already has a reminder, you can:

  • copy that reminder to another record,
  • and then update the properties of the copy.

This works, but there seems to be some lag between the scripted update and the GUI response.

You may have to test the properties of the modified record a couple of times before you see the update registering.

For example:

(() => {
    'use strict';

    const
        dt3 = Application('DEVONthink 3'),
        selectedItems = dt3.selection();


    return selectedItems.length > 1 ? (() => {
        const
            recA = selectedItems[0], // already has a reminder.
            recB = selectedItems[1]; // reminder still null.

        recB.reminder = recA.reminder;

        return [
            recA.reminder.properties(),
            recB.reminder.properties()
        ];

    })() : 'Less than 2 items selected in DEVONthink 3';
})();

And, of course, you can clear a reminder with an incantation like:

rec.reminder = null;

Background:

[Mac Automation Scripting Guide: How Mac Scripting Works](Mac Automation Scripting Guide: How Mac Scripting Works)

The Open Scripting framework defines standard data structures, routines, and resources for creating scripting components, which implement support for specific scripting languages. The AppleScript and JavaScript components (in System/Library/Components), for example, make it possible to control scriptable apps from AppleScript and JavaScript scripts. Through the framework’s standard interface, a scriptable app can interact with any scripting component, regardless of its language. The framework also provides API for compiling, executing, loading, and storing scripts—functions provided by script editing apps.

It’s possible that a diagram lower down on that page, which happens to show the AppleScript component as an example (it could have used any component in System/Library/Components) has been the source of a misapprehension occasionally expressed on these pages :slight_smile:


An advantage of the Open Scripting framework,

which Apple defines (and expects vendors of scriptable applications to implement – hence the JavaScript and other views of the SDEF scripting definitions file available in Script Editor)

is that we can use different tools for different parts of the job, and AppleScript can dial out to JavaScript for help when it lacks libraries.

Similarly, in areas where an SDEF is incompletely compliant, or simply under-tested, we can always use a scrap of AppleScript from JavaScript.

So for example, to script the addition of a reminder to DEVONthink record, we could, from JavaScript:

  1. add the vanilla reminder with an AppleScript incantation
  2. set the remaining reminder properties directly.

For part one of that, you could experiment with things like:

(() => {
    'use strict';

    // main :: IO ()
    const main = () => {
        const
            dt3 = Application('DEVONthink 3'),
            selectedItems = dt3.selection();

        return 0 < selectedItems.length ? (() => {
            const
                rec = selectedItems[0],
                reminder = (
                    addVanillaReminder(rec),
                    rec.reminder
                );
            return rec.reminder.properties()
        })() : 'Nothing selected in DEVONthink 3'
    };

    // addVanillaReminder :: Record -> IO Record
    const addVanillaReminder = rec => {
        const
            databaseID = rec.database.id(),
            recordID = rec.id(),
            asSource = 'tell application "DEVONthink 3" to  ' + (
                `tell content id ${recordID} of ` + (
                    `(database id ${databaseID}) to make ` + (
                        'new reminder with properties ' + (
                            '{due date:current date}\n'
                        )
                    )
                )
            );
        return evalAS(asSource);
    };

    // --------------------- OSA ---------------------

    // evalAS :: String -> Either String a
    const evalAS = s => {
        const
            error = $(),
            result = $.NSAppleScript.alloc.initWithSource(s)
            .executeAndReturnError(error),
            e = ObjC.deepUnwrap(error);
        return e ? (
            e.NSAppleScriptErrorBriefMessage
        ) : ObjC.unwrap(result.stringValue);
    };

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

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

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

For what it’s worth, Apple describes the usage of make in JXA (or rather: not usage thereof) as tersely as possible.
According to it

  let reminder = app.Reminder({dueDate: later, alarm: "notification"});

should work. However, assigning that to r.reminder reliably crashes Script Editor and DT. I’m giving up here - this seems to be one of the cases where JXA just doesn’t cut it. Unfortunately, it is abandonware.

I don’t think the problem is on the Apple side (JXA is just a JSContext with an instance of an Automation library, both of which work fine in other applications. The JSContext is itself an instance of the Safari JavaScript engine, which is continually (and competitively) developed.

When the OSA SDEF is implemented compliantly for an application (as it is, for example, in the case of all the Omni Group apps) anything that works from one OSA language also works from another.

What seems to have happened here is that DEVON struggled a bit (resources ? technical capacity ?) to make their OSA interface compliant when, to quote Apple, in OS X 10.10, JavaScript became a peer to AppleScript in OS X.

The DEVONthink SDEF file still uses AppleScript expressions in inappropriate places – puzzling when viewed as a JS API from Script Editor – and various things have always seemed glitchy and under-tested. There is clearly some avoidance of fixing the peer status of Apple’s scripting languages for Smart Actions.

Probably a bit late in the day to fix DEVONthink’s OSA interface now – if they had the resources and capacity (which, on present showing, seems unlikely) users would get more value from a cross-platform (iOS DEVONthink to Go + macOS DEVONThink) embedded JSContext, allowing for scripts which run on both platforms, on the model of omniJS

(which of course, amongst other things runs very much faster than any AppleScript/JavaScript OSA interface, with all those expensive inter-application Apple Events passing back and forth).

1 Like

I’d guess that you call make as a method of the record, not the application.

Calling it as a method of the record was my first attempt i.e. r.make(...);. When I saw that that ended the app.make(..) in the Script Editor events tab and then starting poking at it in that direction. Almost seemed like it might be possible to call it either way actually.

I hear ya on the lack of docs.

Same experience here with that crash. Also found that referencing external libraries as shown in the Apple release notes also causes Script Editor to crash which makes non-trivial dev problematic.

Puzzling is spot on, have been noticing the glitchy-ness and have been intuiting a feeling of avoidance.

@houthakker, your post is extremely constructive. The observations and example is very useful. Thank you for putting this together, very much appreciated.

1 Like

I’m not really familiar with JXA but the syntax doesn’t seem to be correct, see e.g. javascript - How do I return a manipulated array as text to TextEdit? - Stack Overflow

Therefore theoretically this might work:

Unfortunately the last line crashes the Script Editor.app :face_with_hand_over_mouth:

At least that’s reproducible :wink:
And DT is crashed very reliably by this line, too.

I can see how hard that would make it hard to properly test DEVONthink’s compliance with Apple’s OSA framework.

The problem in not on Apple’s side. Other applications implement the same interface without these errors. Visual Studio Code is not crashed by running the example above, but the automation interaction still fails.

The following is sufficient to reproduce it:

(() => {
    'use strict';

    const
        dt3 = Application('DEVONthink 3'),
        selectedItems = dt3.selection(),
        rec = selectedItems[0];

    selectedItems[0].reminder = new dt3.Reminder();
})();

PS we can already:

  1. Successfully clear an existing reminder by setting its value to null,
  2. and use the Reminder constructor without error,

But the interface has not been made fully compliant with the OSA standard, and we can’t attach the constructed object to the application’s object model.

See sample JS code behind disclosure triangle
(() => {
    'use strict';

    const
        dt3 = Application('DEVONthink 3'),
        selectedItems = dt3.selection(),
        rec = selectedItems[0];

    selectedItems[0].reminder = null

    const newReminder = new dt3.Reminder({
        'week of month': 'no week',
        'alarm': 'no alarm',
        'interval': 1,
        'masc':0,
        'day of week': 'no day',
        'schedule': 'daily',
        'dueDate': new Date()
    })

    selectedItems[0].reminder = newReminder;
})();

And of course it’s also our fault that the Script Editor.app crashes :joy:

2 Likes

Yup. Memory overrun. The Run Script process in Visual Studio is killed too.

Your implementation of the scripting interface is clearly not compliant or stable.

I can understand that it must have been annoying to be ambushed by Yosemite, but that was a little while back, and no one else’s implementation is doing this:

Crashed Thread:        8  com.apple.ScriptEditor.run: Untitled.scpt

Exception Type:        EXC_BAD_ACCESS (SIGSEGV)
Exception Codes:       KERN_INVALID_ADDRESS at 0x000000000000000c
Exception Note:        EXC_CORPSE_NOTIFY

Termination Signal:    Segmentation fault: 11
Termination Reason:    Namespace SIGNAL, Code 0xb
Terminating Process:   exc handler [3521]

I wouldn’t know about that. But I’m fairly certain that it’s not the Script Editor’s fault that DT crashes with the same code :smirk:
On a related note: I found out that the ocr method in JS also does not work as I suppose it should, namely let rec = app.ocr({file: "pathtofile"}); It always complains about a missing parameter. Maybe I’m misreading the documentation or the Script Editor is at fault here :wink:

app.ocr

The AppleScript incantation seems to require a file reference rather than a path string (to avoid the missing parameter response):

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


tell application "DEVONthink 3"
    set fp to "/Users/houthakker/Desktop/Can History Predict the Future?.pdf"
    if my doesFileExist(fp) then
        ocr file fp
    else
        "Not found: " & fp
    end if
end tell


-- doesFileExist :: FilePath -> IO Bool
on doesFileExist(strPath)
    set ca to current application
    set oPath to (ca's NSString's stringWithString:strPath)'s ¬
        stringByStandardizingPath
    set {bln, int} to (ca's NSFileManager's defaultManager's ¬
        fileExistsAtPath:oPath isDirectory:(reference))
    bln and (int ≠ 1)
end doesFileExist

So the JavaScript Automation equivalent should correspondingly require a Path() object.

OS X 10.10 Release Notes - Section on Paths

Supplying a Path does get past the missing parameter message, but then DEVONthink’s implementation of the OSA interface fails to handle the type correctly:

JS Automation library incantation
(() => {
    'use strict';

    const main = () => {
        const fp = '~/Desktop/Can History Predict the Future?.pdf';

        return doesFileExist(fp) ? (
            Application('DEVONthink 3').ocr(
                Path(filePath(fp))
            )
        ) : `File not found: ${fp}`;
    }

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

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

    return main();
})();
// --> Error: Can't convert types.