Count macro runs and save to excel file

Can you help me to create an add-on to the macro that counts every time macro runs and saves a string of date, name of macro and consecutive number of repeats to the same excel file, new row each time?

To see how many times a macro has been used:

Keyboard Maestro > Window > Macro Inspector (⌘4)

to sort macros by use count:

View > Sort Macros by Use Count (^⌘U)

Adding rows to an Excel file will be... tricky. But you could use a CSV file which you'd then import into Excel to analyse:

You'd count "consecutive number of repeats" with a formula in Excel. That would, of course, be consecutive repeats as recorded in your log file -- if you are logging Macro A but not Macro B, executing Macro B wouldn't not reset A's count.

What are you actually trying to achieve? You might be better off parsing the Engine log at regular intervals.

1 Like

Each macro entry in the Keyboard Maestro Macro Stats.plist file includes an .ExecutedCount property, so you could try something like the macro below.

(With a small value for local_Most_Used_N you could list, for example, the 10 most frequently used macros)

N most often used macros.kmmacros (13 KB)


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

    ObjC.import("AppKit");

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

        return [
            `"Uses","Name","Seconds Saved","Duration Saved"`,
            ...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,
                        seconds = Math.floor(stats.TimeSaved),
                        duration = compoundDuration([
                            "wk", "d", "hr", "min", "sec"
                        ])(
                            seconds,
                        )

                    return `${useCount},"${name}",${seconds},"${duration}"`;
                })
        ]
            .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.toSorted(
            (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();
})();