'Hello world' JavaScript for menu of links

Barest bones of a illustrative sample script:

Simply listing all links, and selecting one or more from a bare script-generated menu.

Can be run from Script Editor, with language selector at top left to ‘JavaScript’ rather than ‘AppleScript’.

(() => {
    'use strict';

    ObjC.import('sqlite3');

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

    // Rob Trew 2019

    const main = () => {
        const multipleSelections = true;
        return either(
            msg => msg,
            ks => {
                const sa = standardSEAdditions();
                return ks.map(k => (sa.openLocation(k), k)).join('\n');
            },
            showMenuLR(
                multipleSelections,
                'Hook links',
                linksFromHooKDBPath(
                    '~/Library/Application Support/' +
                    'com.cogsciapps.hook/hook.sqlite'
                )
            )
        );
    };


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

    // linksFromHooKDBPath :: FilePath -> [String]
    const linksFromHooKDBPath = strDBPath => {
        const
            SQLITE_OK = parseInt($.SQLITE_OK, 10),
            SQLITE_ROW = parseInt($.SQLITE_ROW, 10),
            ppDb = Ref(),
            strSQL = 'SELECT src FROM link ORDER by src';

        return bindLR(
            bindLR(
                SQLITE_OK !== $.sqlite3_open(filePath(strDBPath), ppDb) ? (
                    Left($.sqlite3_errmsg(ppDb[0]))
                ) : Right(ppDb[0]),
                db => {
                    const ppStmt = Ref();
                    return SQLITE_OK !== $.sqlite3_prepare_v2(
                        db, strSQL, -1, ppStmt, Ref()
                    ) ? (
                        Left($.sqlite3_errmsg(db))
                    ) : Right(Tuple(db, ppStmt[0]));
                }
            ),

            // Accumulation of all available rows in the table:
            tpl => unfoldr(
                stmt => SQLITE_ROW !== $.sqlite3_step(stmt) ? (
                    $.sqlite3_finalize(stmt),
                    $.sqlite3_close(tpl[0]),
                    Nothing()
                ) : Just(Tuple(
                    $.sqlite3_column_text(stmt, 0),
                    stmt
                )),
                tpl[1]
            )
        );
    };

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

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

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        undefined !== m.Left ? (
            m
        ) : mf(m.Right);

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

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

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

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

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

Interesting exercise – thank you Rob.

Points out an issue with search and discovery. Items meshed with Hook do not have names, for the most part. Makes browsing a chore, perhaps.

We can certainly adjust the SQL to harvest file names as labels for these links.

I suppose one could develop an argument for adding an optional field for mnemonic labels, but then again, perhaps file names play that role anyway ?

We don’t recommend working directly with the database. The schema may change. And Hook interacts dynamically with its DB.

As noted elsewhere on the forum, we do plan to support automation of Hook itself. We will recruit alpha testers and solicit input, and then likely have a public beta.

1 Like