Returning, for example, where N=3, a JSON report from which particular parts can be extracted with the Keyboard Maestro %JSONValue% token, for example:
From:
MRU Macro:
Name: "%JSONValue%local_MRU[1].Name%"
UUID: %JSONValue%local_MRU[1].UUID%
%Variable%local_N% most recent:
%Variable%local_MRU%
To:
MRU Macro:
Name: "N most recently run macros (Name, UUID, Stats)"
UUID: 0CF4715F-6BC4-4768-9042-FEF958612465
3 most recent:
[
{
"UUID": "0CF4715F-6BC4-4768-9042-FEF958612465",
"Name": "N most recently run macros (Name, UUID, Stats)",
"LastExecuted": 746383109.897196,
"ExecutedCount": 15,
"TimeSaved": 932
},
{
"UUID": "D7DB063E-928C-4CD9-B84D-6D4F68498895",
"Name": "Name pasted as Variable",
"LastExecuted": 746382972.560756,
"ExecutedCount": 41,
"TimeSaved": 1036.1400032043457
},
{
"UUID": "10E8D68E-12AB-47FC-B3F9-83C62FDB0023",
"Name": "Compile and run",
"LastExecuted": 746381623.126319,
"ExecutedCount": 119614,
"TimeSaved": 119616
}
]
N most recently run macros (Name- UUID- Stats).kmmacros (11 KB)
Expand disclosure triangle to view JS source
return (() => {
"use strict";
// N most recently used Keyboard Maestro macros
// Ver 0.3 indicates any recently used macros which
// have been deleted since last use.
const main = () =>
kmrecentlyExecutedMacros(
kmvar.local_N
);
const kmrecentlyExecutedMacros = n => {
const
macros = Application("Keyboard Maestro").macros,
fpKMStats = combine(
applicationSupportPath()
)(
"Keyboard Maestro/Keyboard Maestro Macro Stats.plist"
);
return either(
alert("Most recently triggered macros")
)(
xs => xs
)(
fmapLR(
dict => take(n)(
sortDownOn(
uuid => dict[uuid].LastExecuted
)(
Object.keys(dict).filter(k => "All" !== k)
)
)
.map(uuid => {
const macro = macros.byId(uuid);
return {
Name: macro.exists()
? macro.name()
: `n/a [recently deleted]`,
UUID: uuid,
...dict[uuid],
};
})
)(
doesFileExist(fpKMStats)
? jsoFromPlistPathLR(fpKMStats)
: Left(`Stats not found at path: ${fpKMStats}`)
)
);
};
// ----------------------- 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
);
};
// applicationSupportPath :: () -> String
const applicationSupportPath = () => {
const uw = ObjC.unwrap;
return uw(
uw($.NSFileManager.defaultManager
.URLsForDirectoryInDomains(
$.NSApplicationSupportDirectory,
$.NSUserDomainMask
)
)[0].path
);
};
// jsoFromPlistPathLR :: FilePath ->
// Either String Dict
const jsoFromPlistPathLR = fp => {
const
nsDict = $.NSDictionary.dictionaryWithContentsOfURL(
$.NSURL.fileURLWithPath(fp)
);
return nsDict.isNil()
? Left(`Could not be read as .plist: "${fp}"`)
: Right(ObjC.deepUnwrap(nsDict));
};
// --------------------- 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
});
// 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;
};
// doesFileExist :: FilePath -> IO Bool
const doesFileExist = fp => {
const ref = Ref();
return $.NSFileManager
.defaultManager
.fileExistsAtPathIsDirectory(
$(fp).stringByStandardizingPath,
ref
) && !ref[0];
};
// 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);
// 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));
// 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));
// 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));
// take :: Int -> [a] -> [a]
// take :: Int -> String -> String
const take = n =>
// The first n elements of a list,
// string of characters, or stream.
xs => "GeneratorFunction" !== xs
.constructor.constructor.name
? xs.slice(0, n)
: Array.from({ length: n },
() => {
const x = xs.next();
return x.done
? []
: [x.value];
})
.flat();
// sortDownOn :: Ord b => (a -> b) -> [a] -> [a]
const sortDownOn = f =>
// Equivalent to sortBy(flip(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]);
// --------------------- LOGGING ---------------------
// showLog :: a -> IO ()
const showLog = (...args) =>
// eslint-disable-next-line no-console
console.log(
args
.map(JSON.stringify)
.join(" -> ")
);
// sj :: a -> String
const sj = (...args) =>
// Abbreviation of showJSON for quick testing.
// Default indent size is two, which can be
// overriden by any integer supplied as the
// first argument of more than one.
JSON.stringify.apply(
null,
1 < args.length && !isNaN(args[0])
? [args[1], null, args[0]]
: [args[0], null, 2]
);
return sj(main());
})();