Here, for example is a subroutine, and an EXAMPLE macro which uses it.

I happen to have a group called `Bike`

– you will need to test it with the name of one of your macro groups

(Note, both Subroutine and EXAMPLE macro will need to be accessible)

Sorted JSON listing of Group Macros–subroutine and example.kmmacros (12,2 Ko)

For zipping lists of properties together, you will find, that in my JS Prelude library,

RobTrew/prelude-jxa: Generic functions for macOS and iOS scripting in Javascript

in addition to the simplest `zipWith`

(which zips a pair of lists with a custom function) there is also a `zipWith3`

and a `zipWith4`

for three lists and four lists respectively.

In the subroutine above, however, I've used a `ZipList`

approach which can be used with any number of lists (wrapping an additional application of `apZL`

for each additional zipped list, where `apZL`

boils down to `zipWith(identity)`

.

The results of that subroutine are, incidentally, returned in a JSON format, to which the Keyboard Maestro %JSONValue% token can be applied.

##
Expand disclosure triangle to view JS source

```
return (() => {
"use strict";
// main :: IO()
const main = () => {
const
groupName = kmvar.local_Macro_Group_Name,
group = Application("Keyboard Maestro")
.macroGroups.byName(groupName),
sortDirection = toUpper(
kmvar.local_Ascending_or_Descending
)
.startsWith("ASC")
? identity
: flip,
iKey = Number(
kmvar.local_Zero_based_Sort_Key_Index
),
apZL = zipWith(identity);
return either(
alert(`Macros in group '${groupName}'`)
)(
sortBy(
sortDirection(comparing(x => x[iKey]))
)
)(
fmapLR(macros =>
apZL(
apZL(
macros.name()
.map(name => stamp => uuid => [
name, stamp, uuid
])
)(
macros.modificationDate()
)
)(
macros.id()
)
)(
group.exists()
? Right(group.macros)
: Left(
`Group not found as spelled: '${groupName}'.`
)
)
);
};
// --------------------- 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
});
// 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;
};
// 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));
// identity :: a -> a
const identity = x =>
// The identity function.
x;
// 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));
// toUpper :: String -> String
const toUpper = s =>
s.toLocaleUpperCase();
// 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.
xs => ys => xs.slice(
0, Math.min(xs.length, ys.length)
)
.map((x, i) => f(x)(ys[i]));
// ----------------------- 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
);
};
// --------------------- LOGGING ---------------------
// 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());
})();
```