MACRO: List All Macros By Size

FWIW we can also get the listing directly with an XQuery over the plist XML:

XQuery listing of KM Macros by decreasing size.kmmacros (8.7 KB)

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // Rob Trew @2022
    // Ver 0.04

    // Fractionally faster – skips the plutil command line
    // and temporary file.

    // main :: IO ()
    const main = () => {
        const
            uw = ObjC.unwrap,
            fs = "/following-sibling::",
            item = k => `/key['${k}'=string()]${fs}array/dict`,
            name = `/key['Name'=string()]${fs}string[1]/string()`;

        const xquery = `
            for $g in /plist/dict${item("MacroGroups")}
            let $groupName := $g${name}
            for $m in $g/${item("Macros")}
            let $macroName := $m${name}
            let $size := string-length(string($m))
            order by $size descending
            return concat(
                $size,' ',$groupName,' :: ',$macroName
            )`;

        return either(
            alert("XQuery report over KM Macros plist")
        )(
            xs => uw(xs).join("\n")
        )(
            bindLR(
                readPlistFileLR(kmPlistPath())
            )(
                compose(
                    LRBind(
                        compose(
                            LRBind(
                                xQueryOverDocLR(xquery)
                            ),
                            xmlDocFromStringLR
                        )
                    ),
                    plistFromDictLR
                )
            )
        );
    };


    // ---------------- KEYBOARD MAESTRO -----------------

    // kmPlistPath :: () -> IO FilePath
    const kmPlistPath = () => {
        const
            kmMacros = [
                "/Keyboard Maestro/",
                "Keyboard Maestro Macros.plist"
            ].join("");

        return `${applicationSupportPath()}${kmMacros}`;
    };

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

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


    // applicationSupportPath :: () -> String
    const applicationSupportPath = () => {
        const uw = ObjC.unwrap;

        return uw(
            uw($.NSFileManager.defaultManager
                .URLsForDirectoryInDomains(
                    $.NSApplicationSupportDirectory,
                    $.NSUserDomainMask
                )
            )[0].path
        );
    };


    // doesFileExist :: FilePath -> IO Bool
    const doesFileExist = fp => {
        const ref = Ref();

        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(fp)
                .stringByStandardizingPath, ref
            ) && 1 !== ref[0];
    };


    // filePath :: String -> FilePath
    const filePath = s =>
        // The given file path with any tilde expanded
        // to the full user directory path.
        ObjC.unwrap(ObjC.wrap(s)
            .stringByStandardizingPath);


    // plistFromDictLR :: JS Object -> Either String XML
    const plistFromDictLR = jso => {
        const error = $();
        const xml = $.NSString.alloc.initWithDataEncoding(
            $.NSPropertyListSerialization
            .dataWithPropertyListFormatOptionsError(
                $(jso),
                $.NSPropertyListXMLFormat_v1_0, 0,
                error
            ),
            $.NSUTF8StringEncoding
        );

        return xml.isNil() ? Left(
            error.localizedDescription
        ) : Right(
            ObjC.unwrap(xml)
        );
    };


    // readPlistFileLR :: FilePath -> Either String Dict
    const readPlistFileLR = fp =>
        // Either a message or a dictionary of key-value
        // pairs read from the given file path.
        bindLR(
            doesFileExist(fp) ? (
                Right(filePath(fp))
            ) : Left(`No file found at path:\n\t${fp}`)
        )(
            fpFull => {
                const
                    e = $(),
                    maybeDict = $.NSDictionary
                    .dictionaryWithContentsOfURLError(
                        $.NSURL.fileURLWithPath(fpFull),
                        e
                    );

                return maybeDict.isNil() ? (() => {
                    const
                        msg = ObjC.unwrap(
                            e.localizedDescription
                        );

                    return Left(`readPlistFileLR:\n\t${msg}`);
                })() : Right(ObjC.deepUnwrap(maybeDict));
            }
        );


    // xmlDocFromStringLR ::
    // XML String -> Either String NSXMLDocument
    const xmlDocFromStringLR = xml => {
        const
            error = $(),
            xmlDoc = $.NSXMLDocument.alloc
            .initWithXMLStringOptionsError(
                xml, 0, error
            );

        return xmlDoc.isNil() ? (
            Left(ObjC.unwrap(error.localizedDescription))
        ) : Right(xmlDoc);
    };


    // xQueryOverDocLR :: XQuery String ->
    // XMLDoc -> [String]
    const xQueryOverDocLR = xQuery =>
        // List of XQuery result strings for XQuery over doc.
        doc => {
            const
                uw = ObjC.unwrap,
                e = $(),
                xs = doc.objectsForXQueryError(xQuery, e);

            return xs.isNil() ? (
                Left(uw(e.localizedDescription))
            ) : Right(uw(xs).map(uw));
        };


    // --------------------- GENERIC ---------------------

    // 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 => m.Left ? (
            m
        ) : mf(m.Right);


    // LRBind (=<<) :: (a -> Either b) ->
    // Either a -> Either b
    const LRBind = mf =>
        // Flipped version of bindLR
        m => m.Left ? (
            m
        ) : mf(m.Right);


    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
        // A function defined by the right-to-left
        // composition of all the functions in fs.
        fs.reduce(
            (f, g) => x => f(g(x)),
            x => x
        );


    // 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 => e.Left ? (
            fl(e.Left)
        ) : fr(e.Right);


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