Which are my 10 most frequently used macros?

Hey and good day,

(How to find out) which are my 10 most frequently used macros?

I occurred to me that KM is already good with statistics?!

/
with best regards,
Omar KN, Stockholm, Sweden

Select the All Macros smart group, then select View > Sort Macros By > Use Count. Press Command-4 to show the macro inspector, then use the arrow keys to move through the list and you can see how often each macro was used.

I wanted more control over this, though, so I wrote the Macro Usage Tracker macro which lets me decide which macros I want to track, and summarizes them in one table.

-rob.

Hey and good day Rob,

What is the name (for the Macro Usage Tracker macro) of that action/ step, which has to be added to every macro so it will be counted? Didn't find it.

So there is no general reset of the counts, so therefore each macro has to be reset manually. is this correct?
And I have to check/ read how this reset is done.

/
with best regards,
OmarKN

Look at the macro named •main-user | 01 Insert tracking macro call. There are two ways to insert the tracking action: You can copy the red action step and paste it, then enable it in each macro you want to track. Alternatively, you can use the keyboard shortcut (Shift-Control-Option Minus Sign) to insert the action step in whatever macro you're viewing.

Yes—there's no built-in reset mechanism, because I never wanted one (and nobody ever asked for one). To reset them now, you'd have to either manually edit the database in an SQL editor, or just delete the database and start over.

Maybe I'll work on a reset tool, but I've got some other stuff to get done first.

-rob.

Reading directly from Keyboard Maestro's own stats to (for N = 5 for example) something like:

- note file OPEN :: 170571 uses :: 8min saved
- note file for TODAY (Bike) :: 167357 uses :: 8wk 1d 18hr 14min 13sec saved
- Compile and run :: 117363 uses :: 1d 8hr 36min 5sec saved
- Watch focus :: 32069 uses :: 1d 23hr 11min 10sec saved
- Paste as plain text :: 26628 uses :: 3d 12hr 28min 42sec saved

N most often used macros.kmmacros (12 KB)


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

    ObjC.import("AppKit");

    // const kmvar = { "local_Most_Used_N": "10" }

    const main = () => {
        const
            macros = Application("Keyboard Maestro").macros,
            nString = kmvar.local_Most_Used_N,
            n = isNaN(nString)
                ? 10
                : Number(nString),
            fpStats = filePath(
                combine(
                    "~/Library/Application Support/Keyboard Maestro"
                )(
                    "Keyboard Maestro Macro Stats.plist"
                )
            ),
            statDict = ObjC.deepUnwrap(
                $.NSDictionary.dictionaryWithContentsOfURL(
                    $.NSURL.fileURLWithPath(fpStats)
                )
            );

        return sortOn(
            pair => pair[1].ExecutedCount
        )(
            Object.entries(statDict)
        )
            .toReversed()
            .slice(1, 1 + n)
            .map(pair => {
                const
                    name = macros.byId(pair[0]).name().trim(),
                    stats = pair[1],
                    useCount = stats.ExecutedCount,
                    duration = compoundDuration([
                        "wk", "d", "hr", "min", "sec"
                    ])(
                        Math.floor(stats.TimeSaved),
                    )

                return `- ${name} :: ${useCount} uses :: ${duration} saved`;
            })
            .join("\n")
    };




    // ----------------------- JXA -----------------------

    // doesFileExist :: FilePath -> IO Bool
    const doesFileExist = fp => {
        const ref = Ref();

        return $.NSFileManager
            .defaultManager
            .fileExistsAtPathIsDirectory(
                $(fp).stringByStandardizingPath,
                ref
            ) && !ref[0];
    };

    // jsoFromPlistStringLR :: XML String -> Either String Dict
    const jsoFromPlistStringLR = xml => {
        // Either an explanatory message, or a
        // JS dictionary parsed from the plist XML
        const
            e = $(),
            nsDict = $.NSPropertyListSerialization
                .propertyListWithDataOptionsFormatError(
                    $(xml).dataUsingEncoding(
                        $.NSUTF8StringEncoding
                    ),
                    0, 0, e
                );

        return nsDict.isNil()
            ? Left(
                ObjC.unwrap(
                    e.localizedDescription
                )
            )
            : Right(ObjC.deepUnwrap(nsDict));
    };

    // readFileLR :: FilePath -> Either String IO String
    const readFileLR = fp => {
        // Either a message or the contents of any
        // text file at the given filepath.
        const
            uw = ObjC.unwrap,
            e = $(),
            ns = $.NSString
                .stringWithContentsOfFileEncodingError(
                    $(fp).stringByStandardizingPath,
                    $.NSUTF8StringEncoding,
                    e
                );

        return ns.isNil()
            ? Left(uw(e.localizedDescription))
            : Right(uw(ns));
    };


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


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = lr =>
        // Bind operator for the Either option type.
        // If lr has a Left value then lr unchanged,
        // otherwise the function mf applied to the
        // Right value in lr.
        mf => "Left" in lr
            ? lr
            : mf(lr.Right);


    // combine (</>) :: FilePath -> FilePath -> FilePath
    const combine = fp =>
        // The concatenation of two filePath segments,
        // without omission or duplication of "/".
        fp1 => Boolean(fp) && Boolean(fp1)
            ? "/" === fp1.slice(0, 1)
                ? fp1
                : "/" === fp.slice(-1)
                    ? fp + fp1
                    : `${fp}/${fp1}`
            : (fp + fp1);


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


    // compoundDuration :: [String] -> Int -> String
    const compoundDuration = localNames =>
        // A report on compound duration of a quantity of
        // seconds using five name strings for the local
        // equivalents of  "wk", "d", "hr", "min", "sec".
        nSeconds => mapAccumR(r => ([k, n]) => {
            const v = n !== 0 ? (r % n) : r;

            return [
                (r - v) / (n || 1),
                0 < v ? `${v}${k}` : ""
            ];
        })(nSeconds)(
            zip(localNames)([0, 7, 24, 60, 60])
        )[1]
            .filter(Boolean)
            .join(" ");


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


    // filePath :: String -> FilePath
    const filePath = s =>
        // The given file path with any tilde expanded
        // to the full user directory path.
        ObjC.unwrap(
            $(s).stringByStandardizingPath
        );


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


    // mapAccumR :: (acc -> x -> (acc, y)) -> acc ->
    //    [x] -> (acc, [y])
    const mapAccumR = f =>
        // A tuple of an accumulation and a list
        // obtained by a combined map and fold,
        // with accumulation from right to left.
        acc => xs => [...xs].reduceRight(
            ([a, b], x) => second(
                v => [v].concat(b)
            )(
                f(a)(x)
            ),
            Tuple(acc)([])
        );

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


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


    // zip :: [a] -> [b] -> [(a, b)]
    const zip = xs =>
        // The paired members of xs and ys, up to
        // the length of the shorter of the two lists.
        ys => Array.from({
            length: Math.min(xs.length, ys.length)
        }, (_, i) => [xs[i], ys[i]]);

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