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.
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();
})();

