Getting Timestamp of Most Recently Modified Macro

Howdy y’all,

Calling all AppleScript, shell script, JavaScript and any other guru out there to help out with something.

Essentially, I want to get a list of modification dates for every single macro in my library, epoch-formatted, sorted with most recently modified at the top. The approach I am taking works... but it’s SLOW. It takes about 5 seconds on my M3 MacBook Pro (because of the novice AppleScript). :man_facepalming:t2:

There has to be a better way to do this, but it eludes me at the moment. I’m open to suggestions, thanks in advance!

-Chris

Download Macro(s): Get last modified time stamp of all macros in library.kmmacros (4.8 KB)

Macro-Image

Macro-Notes
  • Macros are always disabled when imported into the Keyboard Maestro Editor.
    • The user must ensure the macro is enabled.
    • The user must also ensure the macro's parent macro-group is enabled.
System Information
  • macOS 13.5.1
  • Keyboard Maestro v10.2

Perhaps something like this ?

Download Macro(s): Time stamps of every macro sorted in descending order.kmmacros (3.2 KB)

JS Source Code

(() => {
    "use strict";

    // jxaContext :: IO ()
    const jxaContext = () => {
        const main = () => {
            const
                km = Application('Keyboard Maestro'),
                epochDates = km.macros.modificationDate().map(
                    x => new Date(x).getTime()
                );
            return sortBy(
                flip(compare)
            )(epochDates).join("\n")
        };

        // GENERICS ----------------------------------------------------------------
        // https://github.com/RobTrew/prelude-jxa
        // JS Prelude --------------------------------------------------
        // compare :: a -> a -> Ordering
        const compare = a =>
            b => a < b ? -1 : (a > b ? 1 : 0);

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

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

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

    return jxaContext();
})();
Macro-Image

Macro-Notes
  • Macros are always disabled when imported into the Keyboard Maestro Editor.
    • The user must ensure the macro is enabled.
    • The user must also ensure the macro's parent macro-group is enabled.
System Information
  • macOS 13.4
  • Keyboard Maestro v10.2
6 Likes

Outstanding! That does exactly what I need and has me wanting to learn JavaScript... just can’t seem to find the time to do it. Thanks so much!

-Chris

2 Likes

And, of course, if you needed to at any point, you could extend @unlocked2412 's design to append the corresponding names:

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

    const main = () => {
        const
            km = Application("Keyboard Maestro"),
            macros = km.macros;

        return sortBy(
            flip(compare)
        )(
            zipWith(
                t => k => `${new Date(t).getTime()} -> ${k}`
            )(
                macros.modificationDate()
            )(
                macros.name()
            )
        )
        .join("\n");
    };


    // ------------------ GENERICS -------------------
    // https://github.com/RobTrew/prelude-jxa
    // ----------------- JS PRELUDE ------------------

    // compare :: a -> a -> Ordering
    const compare = a =>
        b => a < b ? -1 : (a > b ? 1 : 0);


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

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


    // zipWith :: (a -> a -> b) -> [a] -> [b]
    const zipWith = f => {
        // A list with the length of the shorter of
        // xs and ys, defined by zipping with a
        // custom function, rather than with the
        // default tuple constructor.
        const go = xs =>
            ys => 0 < xs.length
                ? 0 < ys.length
                    ? [f(xs[0])(ys[0])].concat(
                        go(xs.slice(1))(ys.slice(1))
                    )
                    : []
                : [];

        return go;
    };


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

Wow, I didn't even know I wanted something like this! Thanks all.

2 Likes

Hey @ComplexPoint quick question... is there a way to include the macro UUIDs in the results?

There are a handful of macros that I would like to exclude from this list. I can do this easily enough by removing them from the list after the fact, but to do so reliably, I would like to do this having the list include the macro UUIDs.

If you just want three properties in the list, then I would use zipWith3, and if an arbitrary number then a ZipList object.

zipWith3 first:

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

    const main = () => {
        const
            km = Application("Keyboard Maestro"),
            macros = km.macros;

        return sortBy(
            flip(compare)
        )(
            zipWith3(
                t => k => u =>
                    `${new Date(t).getTime()} -> ${u} -> ${k}`
            )(
                macros.modificationDate()
            )(
                macros.name()
            )(
                macros.id()
            )
        )
        .join("\n");
    };


    // ------------------ GENERICS -------------------
    // https://github.com/RobTrew/prelude-jxa
    // ----------------- JS PRELUDE ------------------

    // compare :: a -> a -> Ordering
    const compare = a =>
        b => a < b ? -1 : (a > b ? 1 : 0);


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


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


    // zipWith3 :: (a -> b -> c -> d) ->
    // [a] -> [b] -> [c] -> [d]
    const zipWith3 = f =>
        xs => ys => zs => Array.from({
            length: Math.min(
                ...[xs, ys, zs].map(x => x.length)
            )
        }, (_, i) => f(xs[i])(ys[i])(zs[i]));


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

Outstanding, this does exactly what I need. I can now filter the results to exclude specific macros, thanks so much!

2 Likes

FWIW you could specify a list of UUIDs to skip at source:

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

    // A list of UUID strings to skip.
    const exclusions = [
        // e.g. as comma-separated strings:
        // "DD24A07A-C07B-4A67-A1E6-0DF8569C473D",
        // "4F71F3BC-4F46-48B2-805F-AEC8EA77E418"
    ];

    const main = () => {
        const
            km = Application("Keyboard Maestro"),
            macros = km.macros;

        return sortBy(
            flip(compare)
        )(
            zipWith3(
                u => t => k =>
                    exclusions.includes(u)
                        ? []
                        : [`${new Date(t).getTime()} -> ${u} -> ${k}`]
            )(
                macros.id()
            )(
                macros.modificationDate()
            )(
                macros.name()
            )
            .flat()
        )
        .join("\n");
    };


    // ------------------ GENERICS -------------------
    // https://github.com/RobTrew/prelude-jxa
    // ----------------- JS PRELUDE ------------------

    // compare :: a -> a -> Ordering
    const compare = a =>
        b => a < b
            ? -1
            : a > b ? 1 : 0;


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


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


    // zipWith3 :: (a -> b -> c -> d) ->
    // [a] -> [b] -> [c] -> [d]
    const zipWith3 = f =>
        xs => ys => zs => Array.from({
            length: Math.min(
                ...[xs, ys, zs].map(x => x.length)
            )
        }, (_, i) => f(xs[i])(ys[i])(zs[i]));


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

This just keeps getting better. :laughing:

For a little background... I have a macro that exports my entire KM library that runs on an idle trigger. But I don’t want it to fully execute every single time. So I have two conditions that it checks for: 1) if the last time it was run was within the last hour, and 2) if the most recently modified macro has not been modified since the last backup.

I use your JavaScript solution to get the most recently modified macros, and if the timestamp for the most recent one hasn’t changed since the last backup (because it saves the timestamp to a global DND variable), then it cancels. I exclude certain macros, because some of them are enabled or disabled as part of my workflow throughout the day, and I don’t want those “changes” to trigger another backup.

If that doesn’t make sense, just look at this screenshot 😅 (click to expand/collapse)

1 Like