External links to tinderbox notes

Seems to be working here after adding a Tinderbox 8 entry in Hook preferences with a Get Name script:

tell application "Tinderbox 8"
    tell front document
        set xs to its selection
        if {} ≠ xs then
            value of (attribute "Name" of (item 1 of xs))
        else
            name of it
        end if
    end tell
end tell

and a Get Address script:

tell application "Tinderbox 8"
    tell front document
        set xs to its selection
        if {} ≠ xs then
            set target to item 1 of xs
        else
            set target to it
        end if
        return value of (attribute "NoteURL" of target)
    end tell
end tell
7 Likes

I was counting on you, Rob! Thanks.

1 Like

Let me know if you think it needs adjustment.

(Perhaps it should be a little more defensive or less ‘partial’ – additionally covering, for example, the case where there is no document open in TB8)

Or more “plain vanilla” AppleScripty versions… (Not yet tested with Hook but results tested to be the same as @ComplexPoint scripts).

tell application "Tinderbox 8"
	tell front document to if it exists then
		tell selection 1 to if it exists then return value of its attribute "Name"
		return value of its attribute "Name" -- "Name" of Tbx document if no note selected
	else
		error "No Tinderbox document open"
	end if
end tell

And…

tell application "Tinderbox 8"
	tell front document to if it exists then
		tell selection 1 to if it exists then return value of its attribute "NoteURL"
		return value of its attribute "NoteURL" -- "NoteURL" of Tbx document if no note selected
	else
		error "No Tinderbox document open"
	end if
end tell

Interesting to learn about Hook!

JXA scripts as well?

1 Like

I’m not sure that Hook is capable of passing on error messages generated by embedded scripts. In my experience if it doesn’t succeed it just fails silently.

In principle one should be able to use JXA with the script starting with //JavaScript. This is a currently undocumented feature. I myself need to get clarity from our developer who added it! I will reply here when I have it.

Coincidentally, we have very recently added a failure notification in an unreleased build – the feature isn’t complete. We will document it in its release and I’ll update here accordingly. It should (at least optionally) pass on the message. Whether to use a macOS notification or just the status panel is a question. Probably should have a preference for this, and perhaps introduce a “Debug Mode” for Hook.

1 Like

Not quite there yet, I think :slight_smile:

At the moment following a //JavaScript line with JS values other than string literals returns either undefined or puzzling URL-encoded strings, which is:

  • encouraging (certainly a sign of some JS evaluation going on)
  • not yet usable, I think, the expected string return mechanism is unclear (and there seems to be a missing urlDecode phase)
\\JavaScript
"hello" + "there"

hellothere

which seems a good start, but then

\\JavaScript
Application("Finder").name()

undefined

and least predictably of all:

this

%5Bobject%20GlobalObject%5D

Which suggests an opacity or slight oversight at some point in the process.

It will be very useful when it settles down, though.

PS a clue to some kind urlencode ⇄ urldecode gap is suggested by the fact that:

\\JavaScript
"hello there"

hello%20there

Updated versions of Hook scripts for Tinderbox:

2 Likes

The scripts designed by RobTrew (@ComplexPoint) were incorporated into Hook’s integration scripts (with minor tweaks) on Dec. 8: Version 94 of the integration scripts, available as an in app update. And they will be in Hook 1.3.3 itself.

Thanks to Rob; we’re delighted to help bridge Tinderbox to all kinds of content on your Macs and beyond.

4 Likes

Thank you for the Hook updates for Tinderbox compatibility,

My main hurdle with Hook is that there is no way to know which notes, or documents, or webpages have been set up with relationships in Hook – especially after time has passed. I was keeping a little notepad (manual) telling me where to look but stopped because that seemed silly.

Not a complete solution, but Hook places a Finder tag on files that have a relationship managed by Hook. So at least all your documents linked to/from can be found. That obviously leaves out webpages and anything managed by applications (Tinderbox, DEVONthink).

That’s helpful – but it’s limited as @ottmar pointed out. As if all that Finder showed us is .txt files. Hook is useful, but not complete – more of a beta, IMO.

Thanks, @PaulWalters and @ottmar. Regarding “not complete – more of a beta, IMO”. Well, Hook is billed mainly as a contextual access tool , as opposed to a search tool — one that provides the missing links. I.e., it’s described mainly as something that helps you link items together, something that was missing from macOS. If it were billed as a search tool, and didn’t provide search, then it would not even be beta. Consider the web: resources, links and a search engine. But the engine (e.g., Duck Duck Go or Google) isn’t for finding links, it’s for finding resources. So Hook doesn’t have a tool for random access to links. It’s more like associative memory than random access memory (that’s just an analogy). It doesn’t replace whatever aggregation tools people have. It’s meant to augment existing software, and help it work better together (link content within and between apps).

Having said that, we’ve had several requests (including here) to view multiple links (and aggregate info beyond 1:1 links), so (likely starting with 1.5) Hook will begin to introduce features along those lines. At that point we’ll expand how the tool is described. Whatever features we add or will add be based on its simple premises (e.g., facilitate contextual navigation of information; augment/leverage/bridge existing software rather than replace or compete; remain lean). Otherwise, the software would bloat. We anticipate several rounds of augmentation re this, some of which has already been described on Hook’s forum.

We’re delighted to have support for Tinderbox and we look forward to hearing how people use it. We’d like to publish some showcases, and we invite people who use Tinderbox and Hook together to share their usage; and we will continue to listen for ways to do better. For instance, we’d like to support multi-select in Tinderbox, which the current integration currently doesn’t (but a script written by RobTrew does, I believe.)

Yes, of course I know Hook isn’t a search tool. My analogy isn’t intended to suggest it is.

My problem: I have a Tinderbox document with several notes linked too other resources (files, etc.) with Hook. And I have many documents in DEVONthink linked similarly with Hook. But a day, a week, a month later when I want to find which of the Tinderbox notes or DEVONthink files have been set up with Hook links there’s no way too figure that out without clicking around, opening and closing Hook to look at its display, etc.

Frustrating. Maybe I just don’t understand the intended use case.

2 Likes

I’m not sure if this would satisfy your needs but for apps that support tagging (e.g., OmniFocus), labels, or related decorations, it would be possible for the integration scripts used by Hook to conditionally add a tag/decoration to the objects they link (as Hook can with Finder files). This could use the same or an additional General setting ("Apply ‘Hook’ tag). In fact, we will be adding a Tags tab to the preference with such controls. So you wouldn’t need to open Hook on an object to see that there’s a link to it. You could tell by looking at the object in front of you in the app with which Hook interoperates.

That feature-set isn’t the same as searching through all your Hook links, though. Searching through Hook links is more along my previous comment. But would it help?

Dependency maintenance (removing tags when the last link to an object is deleted) will be trickier (because Hook links are bidirectional, and Hook only communicates with one app at a time, the foreground app); so we’ll document options closer to doing it.

We’ve also discussed showing tags in the Hook window itself, and cross-app tag navigation (unifying tags from different realms (apps)). As of Hook 1.3, Hook supports in app navigation; we intend to leverage that UI framework to augment contextual navigation.

Thanks, will pay attention as those feature develop.

Meanwhile, one of my holiday activities is reading your book @lucb.

1 Like

Hook turns out in practice to be a perfectly serviceable utility for creating Markdown links to local data.

I agree that it’s hard to find more value than frustration and distraction in the rather over-vaunted database of filename to filename (or URI to URI) links – a pity that the marketing doesn’t just foreground the simpler and more solid value of easier links to local things that might not otherwise be readily linkable.

1 Like

How are the data stored in hook? In a file or in a database? Are they accessible with a search tool like houdahspot or the like?

It’s an sqlite file, not too difficult to script - tho the shelf-life of such a script would clearly be finite.

Of the two experiments which they have run:

  1. A unified approach to making labelled links to local resources,
  2. showing the user only links which have been made to or from the active (front window) resource.

Experiment 1 has succeeded (produced some value), but experiment 2 has failed (produced more frustration and distraction than value), and as the database is still structured around experiment 2, its schema seems very likely to be changed.

Given that caveat, its probably OK to post something like this, (which just lists a menu of all the links created so far, and let us make a selection)

(Tho Luc has expressed understandable uneasiness about users getting direct access to the sqlite3 store of their links)

JavasScript for Automation draft

  • test in Script Editor with top-left language tab set to JavaScript, or in a Keyboard Maestro Execute JavaScript action etc
  • after copying the whole script, scrolling all the way down to return main(); })();
(() => {
    'use strict';

    ObjC.import('sqlite3');

    // HOOK: SIMPLE EXAMPLE OF TABLE-READING FROM JAVASCRIPT FOR AUTOMATION

    // Rob Trew 2019

    const main = () => {
        const multipleSelections = true;
        return sj(either(
            msg => msg,
            xs => {
                const
                    links = nubBy(
                        a => b => snd(a) === snd(b),
                        xs
                    ),
                    addrs = map(fst, links),
                    labels = map(snd, links),
                    sa = standardSEAdditions();
                return bindLR(
                    showMenuLR(true, 'Hook links', labels),
                    choices => map(
                        x => {
                            const
                                strAddr = addrs[elemIndex(x, labels).Just],
                                strURL = strAddr.startsWith('/') ? (
                                    'file://' + strAddr + '/' + x
                                ) : strAddr
                            return (
                                sa.activate(),
                                sa.openLocation(strURL),
                                strURL
                            );
                        },
                        choices
                    )
                );
            },
            linksFromHooKDBPathLR(
                '~/Library/Application Support/' +
                'com.cogsciapps.hook/hook.sqlite'
            )
        ));
    };


    // HOOK.APP - SIMPLEST LISTING OF LINKS VIA SQLITE

    // linkAndLabelFromMeta :: String -> [String]
    const linkAndLabelFromMeta = s => {
        const xs = s.split('$$$');
        return 1 < xs.length ? (
            [xs[0], base64decode(xs[1])]
        ) : [s, ''];
    };

    // linksFromHooKDBPathLR :: FilePath -> Either String [String]
    const linksFromHooKDBPathLR = strDBPath => {
        const
            SQLITE_OK = parseInt($.SQLITE_OK, 10),
            SQLITE_ROW = parseInt($.SQLITE_ROW, 10),
            ppDb = Ref(),
            strSQL =
            'SELECT srcMetaString, destMetaString, ' +
            "COALESCE(path, '') as folder, " +
            'COALESCE(name, "") as fileName ' +
            'FROM link l LEFT JOIN fileinfo f ' +
            'ON l.dest=f.fileid ' +
            'ORDER by src',
            colText = curry($.sqlite3_column_text);

        return bindLR(
            bindLR(
                SQLITE_OK !== $.sqlite3_open(filePath(strDBPath), ppDb) ? (
                    Left($.sqlite3_errmsg(fst(ppDb)))
                ) : Right(fst(ppDb)),
                db => {
                    const ppStmt = Ref();
                    return SQLITE_OK !== $.sqlite3_prepare_v2(
                        db, strSQL, -1, ppStmt, Ref()
                    ) ? (
                        Left($.sqlite3_errmsg(db))
                    ) : Right(Tuple3(
                        db,
                        fst(ppStmt),
                        enumFromTo(
                            0,
                            $.sqlite3_column_count(ppStmt[0]) - 1
                        )
                    ));
                }
            ),
            // (Link, labe) from all available rows in the table:
            tpl => Right(
                sortBy(mappendComparing([snd]),
                    concatMap(
                        x => {
                            const [from, to] = map(
                                linkAndLabelFromMeta,
                                x.slice(0, 1)
                            ).concat([x.slice(2)]);
                            return (0 < (
                                fst(to).length + snd(to).length)) ? (
                                [from, to]
                            ) : [from];
                        },
                        unfoldr(
                            stmt => SQLITE_ROW !== $.sqlite3_step(stmt) ? (
                                $.sqlite3_finalize(stmt),
                                $.sqlite3_close(fst(tpl)),
                                Nothing()
                            ) : Just(
                                Tuple(
                                    map(colText(stmt), tpl[2]),
                                    stmt
                                )
                            ),
                            snd(tpl)
                        )
                    )
                )
            )
        );
    };

    // JXA ------------------------------------------------

    // base64decode :: String -> String
    const base64decode = s =>
        ObjC.unwrap(
            $.NSString.alloc.initWithDataEncoding(
                $.NSData.alloc.initWithBase64EncodedStringOptions(
                    s, 0
                ),
                $.NSUTF8StringEncoding
            )
        );

    // showMenuLR :: Bool -> String -> [String] -> Either String [String]
    const showMenuLR = (blnMult, title, xs) =>
        0 < xs.length ? (() => {
            const sa = standardSEAdditions();
            sa.activate();
            const v = sa.chooseFromList(xs, {
                withTitle: title,
                withPrompt: 'Select' + (
                    blnMult ? ' one or more of ' +
                    xs.length.toString() : ':'
                ),
                defaultItems: xs[0],
                okButtonName: 'OK',
                cancelButtonName: 'Cancel',
                multipleSelectionsAllowed: blnMult,
                emptySelectionAllowed: false
            });
            return Array.isArray(v) ? (
                Right(v)
            ) : Left('User cancelled ' + title + ' menu.');
        })() : Left(title + ': No items to choose from.');

    // standardSEAdditions :: () -> Application
    const standardSEAdditions = () =>
        Object.assign(Application('System Events'), {
            includeStandardAdditions: true
        });

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

    // Just :: a -> Maybe a
    const Just = x => ({
        type: 'Maybe',
        Nothing: false,
        Just: x
    });

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Nothing :: Maybe a
    const Nothing = () => ({
        type: 'Maybe',
        Nothing: true,
    });

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

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

    // compare :: a -> a -> Ordering
    const compare = (a, b) =>
        a < b ? -1 : (a > b ? 1 : 0);

    // comparing :: (a -> b) -> (a -> a -> Ordering)
    const comparing = f =>
        (x, y) => {
            const
                a = f(x),
                b = f(y);
            return a < b ? -1 : (a > b ? 1 : 0);
        };

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

    // curry :: ((a, b) -> c) -> a -> b -> c
    const curry = f => a => b => f(a, b);

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

    // elemIndex :: Eq a => a -> [a] -> Maybe Int
    const elemIndex = (x, xs) => {
        const i = xs.indexOf(x);
        return -1 === i ? (
            Nothing()
        ) : Just(i);
    };

    // enumFromTo :: Int -> Int -> [Int]
    const enumFromTo = (m, n) =>
        Array.from({
            length: 1 + n - m
        }, (_, i) => m + i);

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

    // fst :: (a, b) -> a
    const fst = tpl => tpl[0];

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

    // intercalate :: String -> [String] -> String
    const intercalate = s => xs =>
        xs.join(s);

    // mappendComparing :: [(a -> b)] -> (a -> a -> Ordering)
    const mappendComparing = fs =>
        (x, y) => fs.reduce(
            (ordr, f) => (ordr || compare(f(x), f(y))),
            0
        );

    // map :: (a -> b) -> [a] -> [b]
    const map = (f, xs) =>
        (Array.isArray(xs) ? (
            xs
        ) : xs.split('')).map(f);

    // nubBy :: (a -> a -> Bool) -> [a] -> [a]
    const nubBy = (fEq, xs) => {
        const go = xs => 0 < xs.length ? (() => {
            const x = xs[0];
            return [x].concat(
                go(xs.slice(1)
                    .filter(y => !fEq(x)(y))
                )
            )
        })() : [];
        return go(xs);
    };

    // showJSON :: a -> String
    const sj = x => JSON.stringify(x, null, 2);

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

    // snd :: (a, b) -> b
    const snd = tpl => tpl[1];

    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = (f, xs) =>
        xs.slice()
        .sort(f);

    // unfoldr :: (b -> Maybe (a, b)) -> b -> [a]
    const unfoldr = (f, v) => {
        let
            xr = [v, v],
            xs = [];
        while (true) {
            const mb = f(xr[1]);
            if (mb.Nothing) {
                return xs
            } else {
                xr = mb.Just;
                xs.push(xr[0])
            }
        }
    };

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

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