N Most Recent Macros (run, edited, or created)

N Most Recent Macros (run, edited, or created)

N Most Recent Macros (run- edited- or created).kmmacros (12 KB)


Expand disclosure triangle to view JS source
const
    dateType = kmvar.local_dateType,
    nMRU = Number(kmvar.local_nMostRecent);

return (() => {
    "use strict";

    // N most recent macros in terms of 
    // last run, last edited, or last created.

    // modificationDate
    // creationDate
    // usedDate

    // Rob Trew @2024
    // Ver 0.2

    // dateType drawn from {"created", "edited", "used"}
    const main = () =>
        either(
            alert("First N macros by date of creation edit or use")
        )(
            xs => {
                const
                    sizeColWidth = Math.max(
                        ...xs.map(x => `${x.size}`.length)
                    );

                return [
                    `${nMRU} most recently ${toUpper(dateType)}:`,
                    ...xs.map(x => {
                        const
                            when = taskPaperDateString(x.someDate),
                            sizeCell = `${x.size}`.padStart(sizeColWidth, " ");

                        return `${x.id}\t${sizeCell}\t${when}\t${x.groupName} :: ${x.macroName}`;
                        // OR without id
                        // return `${when}\t${x.groupName} :: ${x.macroName}`;
                    })
                ]
                    .join("\n")

            })(
                mostRecentNMacrosLR(dateType)(nMRU)
            );


    // ----------------- KM MACRO DATES ------------------

    // mostRecentNMacros:: ("created" | "edited" | "used") -> 
    // Int -> String
    const mostRecentNMacrosLR = dateType =>
        n => {
            const
                dateTypes = {
                    "created": "creationDate",
                    "edited": "modificationDate",
                    "used": "usedDate"
                },
                keys = Object.keys(dateTypes);

            return fmapLR(
                macros => sortDownOn(
                    dict => dict.someDate
                )(
                    zipWithN(
                        id => groupName => macroName =>
                            someDate => size =>
                            ({
                                id,
                                groupName,
                                macroName,
                                someDate,
                                size
                            }),
                        macros.id(),
                        macros.macroGroup.name(),
                        macros.name(),
                        macros[dateTypes[dateType]](),
                        macros.size()

                    )
                )
                    .slice(0, n)
            )(
                dateType in dateTypes
                    ? Right(Application("Keyboard Maestro").macros)
                    : Left(`dateType should be drawn from {${keys}}`)
            );
        };

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

    // ------------------- DATE STRING -------------------

    // taskPaperDateString :: Date -> String
    const taskPaperDateString = dte =>
        [...second(t => t.slice(0, 5))(
            iso8601Local(dte).split("T")
        )].join(" ");

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


    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        // A pair of values, possibly of
        // different types.
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });


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


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

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


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


    // iso8601Local :: Date -> String
    const iso8601Local = dte =>
        new Date(dte - (6E4 * dte.getTimezoneOffset()))
            .toISOString();


    // second :: (a -> b) -> ((c, a) -> (c, b))
    const second = f =>
        // A function over a simple value lifted
        // to a function over a tuple.
        // f (a, b) -> (a, f(b))
        xy => Tuple(
            xy[0]
        )(
            f(xy[1])
        );


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


    // sortDownOn :: Ord b => (a -> b) -> [a] -> [a]
    const sortDownOn = f =>
        // Equivalent to sortBy(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]);


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


    // uncurry :: (a -> b -> c) -> ((a, b) -> c)
    const uncurry = f =>
        // A function over a pair, derived
        // from a curried function.
        (...args) => {
            const
                [x, y] = Boolean(args.length % 2)
                    ? args[0]
                    : args;

            return f(x)(y);
        };


    // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
    const zipWith = f =>
        // A list constructed by zipping with a
        // custom function, rather than with the
        // default tuple constructor.
        xs => ys => xs.map(
            (x, i) => f(x)(ys[i])
        ).slice(
            0, Math.min(xs.length, ys.length)
        );


    // zipWithN :: (a -> b -> ... -> d) -> [a], [b] ... -> [d]
    const zipWithN = (f, ...xss) => {
        // Generalisation of ZipWith, ZipWith3 etc.
        // f is a curried function absorbing at least 
        // N arguments, where N is the length of xss.
        const m = 0 < xss.length
            ? Math.min(...xss.map(x => x.length))
            : 0;

        return xss.reduce(
            (gs, vs) => gs.map((g, i) => g(vs[i])),
            Array.from({ length: m }, () => f)
        );
    };

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

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

Or a variant for displaying the group and macro names in a Prompt with List action, and editing any macro that is selected:

Edit one of N Most Recent Macros (run- edited- or created).kmmacros (13 KB)


Expand disclosure triangle to view JS source
const
    dateType = kmvar.local_dateType,
    nMRU = Number(kmvar.local_nMostRecent);

return (() => {
    "use strict";

    // N most recent macros in terms of 
    // last run, last edited, or last created.

    // modificationDate
    // creationDate
    // usedDate

    // Rob Trew @2024
    // Ver 0.3

    // dateType drawn from {"created", "edited", "used"}
    const main = () =>
        either(
            alert("First N macros by date of creation edit or use")
        )(xs => {
            const
                sizeColWidth = Math.max(
                    ...xs.map(x => `${x.size}`.length)
                );

            return xs.map(x => {
                const
                    when = taskPaperDateString(x.someDate),
                    sizeCell = `${x.size}`.padStart(sizeColWidth, "0");

                return `${x.id}__${sizeCell}\t${x.groupName} :: ${x.macroName}`;
            })
                .join("\n")

        })(
            mostRecentNMacrosLR(dateType)(nMRU)
        );


    // ----------------- KM MACRO DATES ------------------

    // mostRecentNMacros:: ("created" | "edited" | "used") -> 
    // Int -> String
    const mostRecentNMacrosLR = dateType =>
        n => {
            const
                dateTypes = {
                    "created": "creationDate",
                    "edited": "modificationDate",
                    "used": "usedDate"
                },
                keys = Object.keys(dateTypes);

            return fmapLR(
                macros => sortDownOn(
                    dict => dict.someDate
                )(
                    zipWithN(
                        id => size => groupName => macroName => someDate =>
                            ({ id, size, groupName, macroName, someDate }),
                        macros.id(),
                        macros.size(),
                        macros.macroGroup.name(),
                        macros.name(),
                        macros[dateTypes[dateType]]()
                    )
                )
                    .slice(0, n)
            )(
                dateType in dateTypes
                    ? Right(Application("Keyboard Maestro").macros)
                    : Left(`dateType should be drawn from {${keys}}`)
            );
        };

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

    // ------------------- DATE STRING -------------------

    // taskPaperDateString :: Date -> String
    const taskPaperDateString = dte =>
        [...second(t => t.slice(0, 5))(
            iso8601Local(dte).split("T")
        )].join(" ");

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


    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        // A pair of values, possibly of
        // different types.
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });


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


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

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


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


    // iso8601Local :: Date -> String
    const iso8601Local = dte =>
        new Date(dte - (6E4 * dte.getTimezoneOffset()))
            .toISOString();


    // second :: (a -> b) -> ((c, a) -> (c, b))
    const second = f =>
        // A function over a simple value lifted
        // to a function over a tuple.
        // f (a, b) -> (a, f(b))
        xy => Tuple(
            xy[0]
        )(
            f(xy[1])
        );


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


    // sortDownOn :: Ord b => (a -> b) -> [a] -> [a]
    const sortDownOn = f =>
        // Equivalent to sortBy(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]);


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


    // uncurry :: (a -> b -> c) -> ((a, b) -> c)
    const uncurry = f =>
        // A function over a pair, derived
        // from a curried function.
        (...args) => {
            const
                [x, y] = Boolean(args.length % 2)
                    ? args[0]
                    : args;

            return f(x)(y);
        };


    // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
    const zipWith = f =>
        // A list constructed by zipping with a
        // custom function, rather than with the
        // default tuple constructor.
        xs => ys => xs.map(
            (x, i) => f(x)(ys[i])
        ).slice(
            0, Math.min(xs.length, ys.length)
        );


    // zipWithN :: (a -> b -> ... -> d) -> [a], [b] ... -> [d]
    const zipWithN = (f, ...xss) => {
        // Generalisation of ZipWith, ZipWith3 etc.
        // f is a curried function absorbing at least 
        // N arguments, where N is the length of xss.
        const m = 0 < xss.length
            ? Math.min(...xss.map(x => x.length))
            : 0;

        return xss.reduce(
            (gs, vs) => gs.map((g, i) => g(vs[i])),
            Array.from({ length: m }, () => f)
        );
    };

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

This is very slick stuff, Rob! Thanks for putting it together.

-rob.

1 Like

Hi @ComplexPoint, thanks for creating this macro.

I noticed that Prompt with List has "Sort Entries" enabled. Once I disabled that, the macro works beautifully. :slight_smile:

1 Like

Thanks – adjusted above.