How to Find/List All Macros Never Used or Used More Than xxx Days Ago?

How to Find/List All Macros Never Used or Used More Than xxx Days Ago?

A listing of all macros, sorted by last use.

Ascending date-time of last use, so dustiest macros at top:

LEAST USED MACROS (all macros sorted by ascending date-time of last use).kmmacros (4.3 KB)


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

    // Macros listed by date-time of last use
    // Least used at top

    // Rob Trew @2024
    // Ver 0.1

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

        // Zip list applicative function.
        const apZip = zipWith(x => x);

        const f = groupName =>
            macroName => usedDate => ({ groupName, macroName, usedDate });

        const
            day = 60 * 60 * 24,
            groupNames = macros.macroGroup.name(),
            macroNames = macros.name(),
            now = new Date(),
            usedDates = macros.usedDate(),
            records = apZip(
                apZip(
                    groupNames.map(f)
                )(macroNames)
            )(usedDates);

        return sortOn(
            dict => dict.usedDate
        )(
            records
        )
            .map(x => `${x.usedDate.toISOString()}\t${x.groupName} :: ${x.macroName}`)
            .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;
        };

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

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

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

Have you tried a Smart Group?

...or

A bit sneaky, but you're simply "omitting any macro used in the last n days/weeks".

3 Likes

Woof!, nice @ComplexPoint ,
To be sure, even if the macro has been called by another macro and has not itself been 'triggered' in the normal sense, will that still constitute it 'being run'?

I'm wanting to clean out my macros and I can start at the top of the list if the above is true and just delete them (a few at a time of course and after backing them up!)

To be sure, even if the macro has been called by another macro and has not itself been 'triggered' in the normal sense, will that still constitute it 'being run'?

Good question.

We would need to ask @peternlewis how to the interpret the Macro.usedDate property.

Empirically, there seem to be a number of macros in my list which are dated with last use at Jan 1 2001, which seemed at first to suggest "unused", but I think a number of them probably have been used.

1 Like

How do we ask? =)
By saying hey @peternlewis can you clarify for us please?

1 Like

A variant listing relative date strings ("unused recently | n days ago | yesterday | today")

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

    // Macros listed by date-time of last use
    // Least used at top

    // Rob Trew @2024
    // Ver 0.2 (relative date in days elapsed, or "unused")

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

        // Zip list applicative function.
        const apZip = zipWith(x => x);

        const f = groupName =>
            macroName => usedDate => ({ groupName, macroName, usedDate });

        const
            now = (new Date()).getTime(),
            day = 1000 * 60 * 60 * 24,

            groupNames = macros.macroGroup.name(),
            macroNames = macros.name(),
            usedDates = macros.usedDate(),
            records = apZip(
                apZip(
                    groupNames.map(f)
                )(macroNames)
            )(usedDates);

        return sortOn(
            dict => dict.usedDate
        )(
            records
        )
            .map(x => {
                const
                    elapsed = Math.round((now - x.usedDate.getTime()) / day),
                    stamp = 8000 < elapsed
                        ? "unused recently"
                        : 0 === elapsed
                            ? "today"
                            : 1 === elapsed
                                ? "yesterday"
                                : `${elapsed} days ago`

                return `${stamp}\t${x.groupName} :: ${x.macroName}`
                // `${x.usedDate.toISOString()}\t${x.groupName} :: ${x.macroName}`
            })
            .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;
        };

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

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

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

Easy enough to test -- and the answer is "Yes, a macro only called via 'Execute a Macro' or 'Execute a Sub-Routine' has its 'Last Used' updated at every execution".

2 Likes

I get an error using this - the second version you provided. No biggie, just FYI

The used date should be the last date the macro was triggered, including via an Execute a Macro or Execute a Subroutine action.

Selecting actions in the macro and clicking Try would not execute the macro, but clicking Run on the macro would, as would triggering it via any means, including any remote means like AppleScript as far as I am aware.

It's possible the macros could have been created and used prior to the features related to Last Used being implemented.

3 Likes

For use with the default Modern Syntax option (behind the small chevron to the left of the code field) we have to precede the code with return :

Screenshot 2024-11-29 at 3.34.50 am

Here is a prewrapped version, as a working macro:

LEAST USED MACROS (descending time elapsed since last use).kmmacros.zip (2,5 Ko)


( The version without the opening return is useful for testing and adjusting in Visual Studio Code, or Script Editor, etc )

3 Likes

Thank you. It works great.

I don’t want to overstay my welcome on this but….
Would it be possible to get that list of unused/oldest used to newest used macros in a clickable html window that would, when clicked take me to that macro in the editor?

Cheers

Stay tuned; after seeing @ComplexPoint's amazing N Most Recent Macros macro this morning, I thought "I'd love to have that in an HTML prompt box."

I'm just about to post it, so hopefully it does what you want.

EDIT: Posted now.

-rob.

2 Likes

Not sure how it works, here I get not results. Do you know why, maybe?

First I tried -use:1000w

(that's 20 years, BTW)

You need to use a Smart Group in the KM Editor, not a Spotlight search.