How do I find a Hook, out of the cold...?

I think you may have hit the central weakness of the current design.

This, FWIW, is the core of what I do:

and as a temporary replacement for the missing set map, I sometimes also run a script to create a menu of all existing links in the Hook database.

This one is in JavaScript for Automation, and can be run from something like Keyboard Maestro, or from Script Editor with the language tab at top left set to JavaScript

JS Script - menu of all links (by name)
(() => {
    'use strict';

    ObjC.import('sqlite3');

    // HOOK: SLIGHTLY FULLER EXAMPLE OF TABLE-READING
    // FROM JAVASCRIPT FOR AUTOMATION
    // (Menu shows labels, follows links)

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

    // Default value (v) if m.Nothing, or f(m.Just)

    // maybe :: b -> (a -> b) -> Maybe a -> b
    const maybe = v => f => m =>
        m.Nothing ? v : f(m.Just);

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

    // until :: (a -> Bool) -> (a -> a) -> a -> a
    const until = p => f => x => {
        let v = x;
        while (!p(v)) v = f(v);
        return v;
    };

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