Tinderbox Forum

JXA: need help with a few things

I think the hang occurs only in an unreleased test version; we’ll watch out for it.

@ComplexPoint: Do you have an idea, how this can be achieved?

I think you should replace

    tinderbox8.documents['xsmart'].notes.push(newNote);

with

   tinderbox8.documents['xsmart'].unshift(newNote);

But it’s been a while since I’ve used Javascript for anything like this.

That was my first idea, but somehow didn’t do the trick.

The thing to be aware of with collections like notes, is that Apple has created some syntactic sugar to make them look a little like JS arrays, but they are really osascript objects.

documents[0] gets pre-preprocessed to the real underlying
documents.at(0)

and
documents['text.tbx'] gets rewritten to
documents.byName('text.tbx')

before the JS interpreter itself actually sees them.

(I personally just use .at(n) and .byName(nameString), finding them slightly less confusing, and even fractionally faster at the last time of testing, though not perceptibly)

Meanwhile the .push and .unshift methods on that object are not really the JS Array methods which they look like, and don’t behave the same way.

We can obtain an actual Array of note values by writing:

documents['text.tbx'].byName('text.tbx').notes()

The trailling ( ) there creates an array. Without it, a plain

documents['text.tbx'].byName('text.tbx').notes

Is a reference to a function object.

(Manipulating an Array of note values, doesn’t however, make any changes to the order of the actual collection of notes in the app)

The note moving pattern is limited, as far as I can see, (in AS and JS) to moving an item into a new container.

So, for example, we could create a new note (the special collection.push() method always appends), and then move it from the end of its current container to become a child of some other note.

Here, we move the freshly created note to become a child of the first note in the document:

(() => {
    'use strict';

    const main = () => {

        // DECLARATIONS
        const
            tinderbox8 = Application('Tinderbox 8'),

            // '~'' expanded to full path.
            fp = filePath('~/Documents/test.tbx'),
            newNote = new tinderbox8.Note({
                name: 'an additional note of some kind'
            });

        // EFFECTS
        tinderbox8.open(fp);
        tinderbox8.activate();

        const
            doc = tinderbox8.documents.byName('test.tbx'),
            notes = doc.notes; // Collection reference, not a JS array.

        notes.push(newNote); // A special 'push', not the Array.push() method.
        tinderbox8.move(
            notes.at(-1), {
                to: notes.at(0)
            }
        );
    };

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

    // filePath :: String -> FilePath
    const filePath = s =>
        ObjC.unwrap(ObjC.wrap(s)
            .stringByStandardizingPath);

    // CALL MAIN FUNCTION
    return main();
})();

Sounds a little bit strange. That would mean, that it is really not possible to move the notes around in the top level? But only move them to another note as a child?

It may be that I just haven’t been a great mover of things and haven’t properly explored.

Anything that is possible in AppleScript will be possible in JS - any AppleScripters here who have experimented with note moving ?

Conceivable I think, that the move method of the osascript interface to Tbx8 is not yet fully implemented.

Putting JS aside for the moment, if I write this AppleScript for OmniOutliner, for example:

tell application "OmniOutliner"
    tell front document
        set oRow to make new row with properties {topic:"Epsilon"}
        move oRow to before first row
    end tell
end tell

The new “Epsilon” row becomes the first child of the document.

If, however, I write the same code for Tinderbox 8, the new note becomes the child, rather than preceding sibling, of the first existing note in the document.

tell application "Tinderbox 8"
    tell front document
        set oNote to make new note with properties {name:"Epsilon"}
        move oNote to before first note
    end tell
end tell

Any thoughts, @eastgate ?

1 Like

Ah, another machine seems to have logged me in, above, with the wrong credentials. Apologies, I will try to delete them.

The question would perhaps also be, what is possible with this “array-like” osascript object? What methods can be applied to it? Is this kind of array or object described anywhere? I could not find anything so far.

The basic reference is this:

https://developer.apple.com/library/archive/releasenotes/InterapplicationCommunication/RN-JavaScriptForAutomation/Articles/OSX10-10.html#//apple_ref/doc/uid/TP40014508-CH109-SW1

‘Element arrays’ section, in particular.

A slightly misleading name, but I think the intention was to adopt idioms that might look familiar to existing users of JS

Not too much information…

For a broader set of overall basics, with JXA and AS in parallel:

https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/index.html#//apple_ref/doc/uid/TP40016239-CH56-SW1

I created a little Alfred workflow, something rather basic… Perhaps this could be a starting point for further little scripting ideas?

At this time, it does the following: When you type “tb {filename}”, you can open the file, reveal the file in the finder or create an inbox item. If the inbox does not exist, it will be created as a top level note (the last one, because we did not find a way to move the note inside the top level… so this has to be done manually).

What I really like about Alfred, is, that he remembers, how often you opened a file. So if you simply hit “tb t” (as a “t” is in every ."t"bx) , you get your TBX files in the order of how often you use them, given that you opened the files with Alfred, of course. Really nice.

The creation of the text of the note does not work somehow. Somewhere I read, that properties for the scripting does not exist? Yet? Is that right, @eastgate?

So, here is my first draft of the workflow:
tinderbox8.alfredworkflow.zip (607.3 KB)

@ComplexPoint: If you like, you may rewrite the code. I am aware, that it is not elegant at all (Kamikaze noob style :joy:… but I try getting better)

Looks good :slight_smile:

FWIW, another way of checking whether an ‘Inbox’ note already exists is to use a whose/where query.

(() => {
    'use strict';

    const
        tbx = Application('Tinderbox 8'),
        doc = tbx.documents.at(0);

    // inboxFoundOrCreated :: TBX App -> TBX Doc -> TBX Note
    const inboxFoundOrCreated = app => doc => {
        const
            xs = doc.notes.where({
                name: 'Inbox'
            });
        return 0 < xs.length ? ( // Found ?
            xs.at(0)
        ) : (() => { // Or created:
            const inboxNote = new tbx.Note({
                name: 'Inbox'
            });
            return (
                doc.notes.push(inboxNote),
                inboxNote
            );
        })();
    };


    return inboxFoundOrCreated(tbx)(doc).name();
})();

The text property may not be settable in an argument to the Note() constructor, but once a note is created, you should be able to set the text attribute value by writing something like:

oNote.text = 'Some textual value';

Is there a better way to pass arguments into another script?
How would this work in this arrow function?
I tried something like ((argv) => {…, but this did not work

Tell me more ?

Could you show me the whole thing that you were experimenting with, with a quick note on what you are after ?

(I grow old, and showing works better than telling :slight_smile: )

A very basic pattern would be:

(() => {
    'use strict';

    // Function definition

    // dbl :: Int -> Int
    const dbl = x =>
        2 * x;


    // Function call

    return dbl(7);
    // -> 14
})();

but I’m not sure if that’s what you’re looking for.

Ah, perhaps you are thinking of this. See argument passed in last line:

(argv => {
    'use strict';

    // Function definition

    // dbl :: Int -> Int
    const dbl = x =>
        2 * x;


    // Function call

    return dbl(argv);
    // -> 542
    
})(271);

I pass an argument from Alfred to the script with:

function run(argv) {...,

where I pass the document name and I wanted to know, how this would work with your arrow function.

Small functions can be declared inside the big functions that use them, so for example:

function run(argv) {
    'use strict';

    // MAIN FUNCTION, CALLED AT FOOT OF RUN()
    // (after the definition of any utility functions which it needs)

    // main :: () -> IO String
    const main = () => {
        const
            tbx = Application('Tinderbox 8'),
            ds = tbx.documents,
            doc = 0 < ds.length ? (
                ds.at(0)
            ) : undefined;

        if (undefined !== doc) {
            tbx.activate();
            const inbox = inboxFoundOrCreated(tbx)(doc);

            return inbox.name();
        } else {
            return "No Tinderbox 8 docs open."
        }
    };


    // UTILITY FUNCTIONS -------------------------

    // inboxFoundOrCreated :: TBX App -> TBX Doc -> TBX Note
    const inboxFoundOrCreated = app => doc => {
        const
            xs = doc.notes.where({
                name: 'Inbox'
            });
        return 0 < xs.length ? ( // Found ?
            xs.at(0)
        ) : (() => { // Or created:
            const inboxNote = new app.Note({
                name: 'Inbox'
            });
            return (
                doc.notes.push(inboxNote),
                inboxNote
            );
        })();
    };

    return main();
}