N most recently run macros (Name, UUID, Stats)

Returning, for example, where N=3, a JSON report from which particular parts can be extracted with the Keyboard Maestro %JSONValue% token, for example:

From:

MRU Macro:
    Name: "%JSONValue%local_MRU[1].Name%"
    UUID: %JSONValue%local_MRU[1].UUID%

%Variable%local_N% most recent:
%Variable%local_MRU%

To:

MRU Macro:
	Name: "N most recently run macros (Name, UUID, Stats)"
	UUID: 0CF4715F-6BC4-4768-9042-FEF958612465

3 most recent:
[
  {
    "UUID": "0CF4715F-6BC4-4768-9042-FEF958612465",
    "Name": "N most recently run macros (Name, UUID, Stats)",
    "LastExecuted": 746383109.897196,
    "ExecutedCount": 15,
    "TimeSaved": 932
  },
  {
    "UUID": "D7DB063E-928C-4CD9-B84D-6D4F68498895",
    "Name": "Name pasted as Variable",
    "LastExecuted": 746382972.560756,
    "ExecutedCount": 41,
    "TimeSaved": 1036.1400032043457
  },
  {
    "UUID": "10E8D68E-12AB-47FC-B3F9-83C62FDB0023",
    "Name": "Compile and run",
    "LastExecuted": 746381623.126319,
    "ExecutedCount": 119614,
    "TimeSaved": 119616
  }
]

N most recently run macros (Name- UUID- Stats).kmmacros (11 KB)


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

    // N most recently used Keyboard Maestro macros
    // Ver 0.3 indicates any recently used macros which
    //         have been deleted since last use.

    const main = () =>
        kmrecentlyExecutedMacros(
            kmvar.local_N
        );

    const kmrecentlyExecutedMacros = n => {
        const
            macros = Application("Keyboard Maestro").macros,
            fpKMStats = combine(
                applicationSupportPath()
            )(
                "Keyboard Maestro/Keyboard Maestro Macro Stats.plist"
            );

        return either(
            alert("Most recently triggered macros")
        )(
            xs => xs
        )(
            fmapLR(
                dict => take(n)(
                    sortDownOn(
                        uuid => dict[uuid].LastExecuted
                    )(
                        Object.keys(dict).filter(k => "All" !== k)
                    )
                )
                    .map(uuid => {
                        const macro = macros.byId(uuid);

                        return {
                            Name: macro.exists()
                                ? macro.name()
                                : `n/a [recently deleted]`,
                            UUID: uuid,
                            ...dict[uuid],
                        };
                    })
            )(
                doesFileExist(fpKMStats)
                    ? jsoFromPlistPathLR(fpKMStats)
                    : Left(`Stats not found at path: ${fpKMStats}`)
            )
        );
    };

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


    // jsoFromPlistPathLR :: FilePath -> 
    // Either String Dict 
    const jsoFromPlistPathLR = fp => {
        const
            nsDict = $.NSDictionary.dictionaryWithContentsOfURL(
                $.NSURL.fileURLWithPath(fp)
            );

        return nsDict.isNil()
            ? Left(`Could not be read as .plist: "${fp}"`)
            : Right(ObjC.deepUnwrap(nsDict));
    };

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


    // combine (</>) :: FilePath -> FilePath -> FilePath
    const combine = fp =>
        // The concatenation of two filePath segments,
        // without omission or duplication of "/".
        fp1 => Boolean(fp) && Boolean(fp1)
            ? "/" === fp1.slice(0, 1)
                ? fp1
                : "/" === fp.slice(-1)
                    ? fp + fp1
                    : `${fp}/${fp1}`
            : (fp + fp1);

    // comparing :: Ord a => (b -> a) -> b -> b -> Ordering
    const comparing = f =>
        // The ordering of f(x) and f(y) as a value
        // drawn from {-1, 0, 1}, representing {LT, EQ, GT}.
        x => y => {
            const
                a = f(x),
                b = f(y);

            return a < b
                ? -1
                : a > b
                    ? 1
                    : 0;
        };


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

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


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


    // flip :: (a -> b -> c) -> b -> a -> c
    const flip = op =>
        // The binary function op with
        // its arguments reversed.
        1 !== op.length
            ? (a, b) => op(b, a)
            : (a => b => op(b)(a));


    // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c
    const fmapLR = f =>
        // Either f mapped into the contents of any Right
        // value in e, or e unchanged if is a Left value.
        e => "Left" in e
            ? e
            : Right(f(e.Right));


    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = f =>
        // A copy of xs sorted by the comparator function f.
        xs => xs.slice()
            .sort((a, b) => f(a)(b));


    // take :: Int -> [a] -> [a]
    // take :: Int -> String -> String
    const take = n =>
        // The first n elements of a list,
        // string of characters, or stream.
        xs => "GeneratorFunction" !== xs
            .constructor.constructor.name
            ? xs.slice(0, n)
            : Array.from({ length: n },
                () => {
                    const x = xs.next();

                    return x.done
                        ? []
                        : [x.value];
                })
                .flat();





    // sortDownOn :: Ord b => (a -> b) -> [a] -> [a]
    const sortDownOn = f =>
        // Equivalent to sortBy(flip(comparing(f))), but with f(x)
        // evaluated only once for each x in xs.
        // ('Schwartzian' decorate-sort-undecorate).
        xs => sortBy(
            flip(comparing(x => x[0]))
        )(
            xs.map(x => [f(x), x])
        )
            .map(x => x[1]);

    // --------------------- LOGGING ---------------------


    // showLog :: a -> IO ()
    const showLog = (...args) =>
        // eslint-disable-next-line no-console
        console.log(
            args
                .map(JSON.stringify)
                .join(" -> ")
        );

    // sj :: a -> String
    const sj = (...args) =>
        // Abbreviation of showJSON for quick testing.
        // Default indent size is two, which can be
        // overriden by any integer supplied as the
        // first argument of more than one.
        JSON.stringify.apply(
            null,
            1 < args.length && !isNaN(args[0])
                ? [args[1], null, args[0]]
                : [args[0], null, 2]
        );

    return sj(main());
})();
2 Likes

Updated to Ver 0.3, above, to allow for the fact (kudos to @_jims for catching this) that the Keyboard Maestro Macro Stats.plist may still contain usage stats for macros which the user has since deleted.

The names of deleted macros can not be retrieved, and are displayed as:

 "Name": "n/a [recently deleted]"
2 Likes