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).

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!

(() => {
"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();
})();

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

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

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

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)