FWIW we can also get the listing directly with an XQuery over the plist XML:
XQuery listing of KM Macros by decreasing size.kmmacros (8.7 KB)
Expand disclosure triangle to view JS Source
(() => {
"use strict";
// Rob Trew @2022
// Ver 0.04
// Fractionally faster – skips the plutil command line
// and temporary file.
// main :: IO ()
const main = () => {
const
uw = ObjC.unwrap,
fs = "/following-sibling::",
item = k => `/key['${k}'=string()]${fs}array/dict`,
name = `/key['Name'=string()]${fs}string[1]/string()`;
const xquery = `
for $g in /plist/dict${item("MacroGroups")}
let $groupName := $g${name}
for $m in $g/${item("Macros")}
let $macroName := $m${name}
let $size := string-length(string($m))
order by $size descending
return concat(
$size,' ',$groupName,' :: ',$macroName
)`;
return either(
alert("XQuery report over KM Macros plist")
)(
xs => uw(xs).join("\n")
)(
bindLR(
readPlistFileLR(kmPlistPath())
)(
compose(
LRBind(
compose(
LRBind(
xQueryOverDocLR(xquery)
),
xmlDocFromStringLR
)
),
plistFromDictLR
)
)
);
};
// ---------------- KEYBOARD MAESTRO -----------------
// kmPlistPath :: () -> IO FilePath
const kmPlistPath = () => {
const
kmMacros = [
"/Keyboard Maestro/",
"Keyboard Maestro Macros.plist"
].join("");
return `${applicationSupportPath()}${kmMacros}`;
};
// ----------------------- 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
);
};
// doesFileExist :: FilePath -> IO Bool
const doesFileExist = fp => {
const ref = Ref();
return $.NSFileManager.defaultManager
.fileExistsAtPathIsDirectory(
$(fp)
.stringByStandardizingPath, ref
) && 1 !== ref[0];
};
// filePath :: String -> FilePath
const filePath = s =>
// The given file path with any tilde expanded
// to the full user directory path.
ObjC.unwrap(ObjC.wrap(s)
.stringByStandardizingPath);
// plistFromDictLR :: JS Object -> Either String XML
const plistFromDictLR = jso => {
const error = $();
const xml = $.NSString.alloc.initWithDataEncoding(
$.NSPropertyListSerialization
.dataWithPropertyListFormatOptionsError(
$(jso),
$.NSPropertyListXMLFormat_v1_0, 0,
error
),
$.NSUTF8StringEncoding
);
return xml.isNil() ? Left(
error.localizedDescription
) : Right(
ObjC.unwrap(xml)
);
};
// readPlistFileLR :: FilePath -> Either String Dict
const readPlistFileLR = fp =>
// Either a message or a dictionary of key-value
// pairs read from the given file path.
bindLR(
doesFileExist(fp) ? (
Right(filePath(fp))
) : Left(`No file found at path:\n\t${fp}`)
)(
fpFull => {
const
e = $(),
maybeDict = $.NSDictionary
.dictionaryWithContentsOfURLError(
$.NSURL.fileURLWithPath(fpFull),
e
);
return maybeDict.isNil() ? (() => {
const
msg = ObjC.unwrap(
e.localizedDescription
);
return Left(`readPlistFileLR:\n\t${msg}`);
})() : Right(ObjC.deepUnwrap(maybeDict));
}
);
// xmlDocFromStringLR ::
// XML String -> Either String NSXMLDocument
const xmlDocFromStringLR = xml => {
const
error = $(),
xmlDoc = $.NSXMLDocument.alloc
.initWithXMLStringOptionsError(
xml, 0, error
);
return xmlDoc.isNil() ? (
Left(ObjC.unwrap(error.localizedDescription))
) : Right(xmlDoc);
};
// xQueryOverDocLR :: XQuery String ->
// XMLDoc -> [String]
const xQueryOverDocLR = xQuery =>
// List of XQuery result strings for XQuery over doc.
doc => {
const
uw = ObjC.unwrap,
e = $(),
xs = doc.objectsForXQueryError(xQuery, e);
return xs.isNil() ? (
Left(uw(e.localizedDescription))
) : Right(uw(xs).map(uw));
};
// --------------------- 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
});
// bindLR (>>=) :: Either a ->
// (a -> Either b) -> Either b
const bindLR = m =>
mf => m.Left ? (
m
) : mf(m.Right);
// LRBind (=<<) :: (a -> Either b) ->
// Either a -> Either b
const LRBind = mf =>
// Flipped version of bindLR
m => m.Left ? (
m
) : mf(m.Right);
// compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
const compose = (...fs) =>
// A function defined by the right-to-left
// composition of all the functions in fs.
fs.reduce(
(f, g) => x => f(g(x)),
x => x
);
// 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 => e.Left ? (
fl(e.Left)
) : fr(e.Right);
// MAIN ---
return main();
})();
