/prelude-jxa

Generic functions for macOS and iOS scripting in Javascript – function names as in Hoogle

Primary LanguageJavaScriptMIT LicenseMIT

prelude-jxa

Generic functions for:

  • macOS scripting with JavaScript for Automation
  • iOS scripting in JavaScript, with apps like the excellent 1Writer, and @agiletortoise's Drafts.

Details:

  • Function names are as in Hoogle.
  • The 400+ functions in jsPrelude.js are generic and cross-platform (macOS, iOS etc),
  • The 20+ functions in jxaSystemIO.js are specific to macOS.

For the purposes of sketching and testing a script,
the JavaScriptCore interpreter used on macOS and iOS is fast enough to allow for import of the whole of the jsPrelude.js file and, in the case of macOS, the jxaSystemIO.js file as well.

(c. 500 generic and file-system functions in total)

Display a menu of functions to copy to the clipboard

Menu of functions

(() => {
    'use strict';
    
    // Display a menu of functions to select and copy.
    // Rob Trew @2020

    ObjC.import('AppKit')

    // ---------------------- MAIN ----------------------
    const main = () => {
        const inner = () => {
            const
                fpFolder = '~/prelude-jxa',
                menuJSONFile = 'jsPreludeMenu.json';
            return either(
                alert('JavaScript functions')
            )(
                // Copied to clipboard and returned.
                x => (
                    copyText(x),
                    x
                )
            )(
                bindLR(
                    readFileLR(
                        combine(fpFolder)(menuJSONFile)
                    )
                )(
                    json => bindLR(
                        jsonParseLR(json)
                    )(
                        dict => bindLR(
                            showMenuLR(true)(
                                'Functions'
                            )(Object.keys(dict))
                        )(
                            ks => Right(
                                ks.map(k => dict[k])
                                .join('\n\n\n')
                            )
                        )
                    )
                )
            )
        };

        // alert :: String => String -> IO String
        const alert = title =>
            s => {
                const sa = Object.assign(
                    Application('System Events'), {
                        includeStandardAdditions: true
                    });
                return (
                    sa.activate(),
                    sa.displayDialog(s, {
                        withTitle: title,
                        buttons: ['OK'],
                        defaultButton: 'OK'
                    }),
                    s
                );
            };

        // copyText :: String -> IO String
        const copyText = s => {
            const pb = $.NSPasteboard.generalPasteboard;
            return (
                pb.clearContents,
                pb.setStringForType(
                    $(s),
                    $.NSPasteboardTypeString
                ),
                s
            );
        };

        return inner();
    };

    // ---------------- JS PRELUDE - JXA ----------------

    // readFileLR :: FilePath -> Either String IO String
    const readFileLR = fp => {
        const
            e = $(),
            ns = $.NSString
            .stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );
        return ns.isNil() ? (
            Left(ObjC.unwrap(e.localizedDescription))
        ) : Right(ObjC.unwrap(ns));
    };


    // ------------------- JS PRELUDE -------------------

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

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

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

    // combine (</>) :: FilePath -> FilePath -> FilePath
    const combine = fp =>
        // Two paths combined with a path separator. 
        // Just the second path if that starts 
        // with a path separator.
        fp1 => Boolean(fp) && Boolean(fp1) ? (
            '/' === fp1.slice(0, 1) ? (
                fp1
            ) : '/' === fp.slice(-1) ? (
                fp + fp1
            ) : fp + '/' + fp1
        ) : fp + fp1;

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => 'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

    // jsonParseLR :: String -> Either String a
    const jsonParseLR = s => {
        try {
            return Right(JSON.parse(s));
        } catch (e) {
            return Left(
                `${e.message} (line:${e.line} col:${e.column})`
            );
        }
    };

    // showMenuLR :: Bool -> String -> [String] -> 
    // Either String [String]
    const showMenuLR = blnMult =>
        title => xs => 0 < xs.length ? (() => {
            const sa = Object.assign(
                Application('System Events'), {
                    includeStandardAdditions: true
                });
            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.');

    return main();
})();