External links to tinderbox notes

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

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 real value), but experiment 2 has failed (produced more frustration and distraction than value), and as the database is 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).

JavaScript for Automation

  • Test in Script Editor with top-left language tab set to JavaScript, or in a Keyboard Maestro Execute JavaScript action, etc
  • after selecting and copying the whole source, scrolling all the way down to // MAIN --- 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();
})();
2 Likes

Nice :grinning:

Having this knowledge, you could also easily use the sqlite browser to display the DB. There you can also filter or start some simple SQL queries…
In any case: some good starting points to get access to the stored data

1 Like

Yes, but end user contributions like this can be fragile in the short run.

1 Like

user contributions like this can be fragile in the short run.

Yes, I think that’s very fair.

Practitioner coding can work well for the individual practitioner – making and maintaining any custom tools they need for projects in hand – but it’s not a very sustainable way to keep a whole community of users supplied with solidly working basics. That really needs a business model.

1 Like

Hook has an “export” file but it’s just an XML list of URL pairs. In addition to XML, if there was a CSV or TSV or OPML export that included more data about the links (document or page name, date of link, etc.) then that would help a lot with the discovery issue. Would be excellent for Tinderbox users.

1 Like

FWIW, here is a little Alfred workflow to search for the Hook stuff.

There are 3 kinds of searches:

  • “hl” looks only for links
  • “hf” looks only for linked files
  • “hk” looks for links and linked files

(“hl” and “hf” are actually part of “hk”, but perhaps I only look for a special kind of the two)

Those keywords can be combined with a search string like:

"hl" + <space> + <search string>

which will look for the search string in the links and the titles as well in the file names, when using “hf” or “hk”.

This should look like so:


When using

<keyword> + <space> + <space>

the result is not filtered, so there could be a lot of hits.
Simply using

"hktsv"

as Alfred command should create a file Hook.tsv in the ~/Downloads folder with all the links, files, folders and the creation date. It should be pasteable into a TB document, where the titles of the links become the $Name of the note.

I don’t know, if it works for you, and especially I don’t know, how it works with large amounts of data, as my DB is quite small so far.

Many thanks to Rob Trew, who gave me some of the ideas for this little workflow :grinning:

So, and here it is:

Hook Search.alfredworkflow.zip (126.9 KB)

2 Likes

Just found a little issue: The tsv file can not be written properly due to unicode problems in python 2. Have to look for a solution…

Edit: Fixed! Seems to work now. New version uploaded in the post above

I named the columns of the tsv file in a way, that most of the attributes will be built-in attributes and if the file is copied into a tbx document, it will look like this:

As the creation of this workflow is still going on, I will always add some new features (when they come to my mind :thinking:):

Now you can add a shortcut for each part of the workflow, if you like, and when you press the “option”-key while searching for a link or a file (or both), the Hook window will open 2 seconds after the search.

Revisiting this topic of several years ago, FYI in 2020 Hook gained a search tool, and also an AppleScript interface. We also published software to use with Alfred and LaunchBar. There’s also a CLI.

2 Likes