List Macros With HotkeyTriggers

A variant on this theme which lists macros:

  • by group,
  • with assigned hotkeys in a left-hand column.

Groups of Macros listed with hotkey column to left.kmmacros (8.6 KB)


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

    // Listing of all Keyboard Maestro macros by group
    // Any assigned hotkey(s) listed in column to left.

    // Rob Trew @2024

    // ---------------------- MAIN -----------------------
    const main = () => {
        const
            macros = Application("Keyboard Maestro").macros,

            keyLabels = macros.triggers.description()
                .map(hotkeys),

            keyColWidth = Math.max(
                ...keyLabels.flat().map(x => x.length)
            );

        return groupOnKey(x => x.groupName)(
            sortOn(x => x.groupName)(
                zipWithN(
                    groupName => macroName => triggers => ({
                        groupName, macroName, triggers
                    }),
                    macros.macroGroup.name(),
                    macros.name(),
                    keyLabels
                )
            )
        )
            .map(groupListing(keyColWidth))
            .join("\n")
    };

    // --------------- MACRO GROUP LISTING ---------------

    // groupListing :: Int -> (String, [Macro]) -> String
    const groupListing = keyColWidth =>
        ([groupName, macros]) => [
            `\n${toUpper(groupName)}`,
            ...macros.flatMap(macro => {
                const
                    hs = macro.triggers,
                    n = hs.length,
                    xs = zipWithLong(
                        k => name =>
                            `${k.padEnd(keyColWidth)}\t${name}`
                    )(
                        hs
                    )([
                        macro.macroName
                    ]);

                return 0 < n
                    ? indentedText(xs)
                    : `\t${" ".repeat(keyColWidth)}${indentedText(xs)}`;
            })
        ]
            .join("\n");


    // hotkeys :: [String] -> [String]
    const hotkeys = xs =>
        xs.flatMap(
            k => k.startsWith("The Hot Key")
                ? [keyPart(k)]
                : []
        );


    // keyPart :: String -> String
    const keyPart = s =>
        s.slice(12, -11);


    // indentedText :: [String] -> String
    const indentedText = xs =>
        xs.map(x => `\t${x}`)
            .join("\n");


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

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


    // groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
    const groupBy = eqOp =>
        // A list of lists, each containing only elements
        // equal under the given equality operator, such
        // that the concatenation of these lists is xs.
        xs => 0 < xs.length
            ? (() => {
                const [h, ...t] = xs;
                const [groups, g] = t.reduce(
                    ([gs, a], x) => eqOp(a[0])(x)
                        ? [gs, [...a, x]]
                        : [[...gs, a], [x]],
                    [[], [h]]
                );

                return [...groups, g];
            })()
            : [];


    // groupOnKey :: Eq k => (a -> k) -> [a] -> [(k, [a])]
    const groupOnKey = f =>
        // A list of (k, [a]) tuples, in which each [a]
        // contains only elements for which f returns the
        // same value, and in which k is that value.
        // The concatenation of the [a] in each tuple === xs.
        xs => 0 < xs.length
            ? groupBy(a => b => a[0] === b[0])(
                xs.map(x => [f(x), x])
            )
                .map(gp => [
                    gp[0][0],
                    gp.map(ab => ab[1])
                ])
            : [];


    // maximumBy :: (a -> a -> Ordering) -> [a] -> a
    const maximumBy = f =>
        xs => 0 < xs.length
            ? xs.slice(1).reduce(
                (a, x) => 0 < f(x)(a)
                    ? x
                    : a,
                xs[0]
            )
            : undefined;

    // length :: [a] -> Int
    const length = xs =>
        // Returns Infinity over objects without finite
        // length. This enables zip and zipWith to choose
        // the shorter argument when one is non-finite,
        // like cycle, repeat etc
        "Node" !== xs.type
            ? "GeneratorFunction" !== (
                xs.constructor.constructor.name
            )
                ? xs.length
                : Infinity
            : lengthTree(xs);


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


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


    // toUpper :: String -> String
    const toUpper = s =>
        s.toLocaleUpperCase();


    // zipWithLong :: (a -> a -> a) -> [a] -> [a] -> [a]
    const zipWithLong = f => {
        // A list with the length of the *longer* of
        // xs and ys, defined by zipping with a
        // custom function, rather than with the
        // default tuple constructor.
        // Any unpaired values, where list lengths differ,
        // are simply appended.
        const go = xs =>
            ys => 0 < xs.length
                ? 0 < ys.length
                    ? [f(xs[0])(ys[0])].concat(
                        go(
                            xs.slice(1)
                        )(
                            ys.slice(1)
                        )
                    )
                    : xs
                : ys;

        return go;
    };


    // zipWithN :: (a -> b -> ... -> c) -> ([a], [b] ...) -> [c]
    const zipWithN = (...args) =>
        // Uncurried function of which the first argument is a
        // curried function, and all remaining arguments are lists.
        1 < args.length
            ? (
                ([f, ...xs]) => xs.slice(1).reduce(
                    // apZip
                    (gs, vs) => gs.map((g, i) => g(vs[i])),
                    xs[0].map(f)
                )
            )(args)
            : [];

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