For reference, the JS source, to be run in a KM Execute JavaScript for Automation action, as above.
If you want to test the code outside a running macro, you should obtain the UUID of the target macro with KM Editor > Edit > Copy As > Copy UUID, and paste it in the place indicated at line 236 of this source code.
(When the code is running inside a macro, the correct UUID is obtained automatically)
(() => {
"use strict";
// Run-time translation of a Keyboard Maestro FOR EACH statement-action
// to a MAP expression-action.
// (returning the value of a function applied across a collection)
// GENERIC FUNCTIONS -----------------------------------------------------
// bindMay (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
const bindMay = (mb, mf) =>
mb.nothing ? mb : mf(mb.just);
// concatMap :: (a -> [b]) -> [a] -> [b]
const concatMap = (f, xs) =>
xs.length > 0 ? [].concat.apply([], xs.map(f)) : [];
// foldl :: (a -> b -> a) -> a -> [b] -> a
const foldl = (f, a, xs) => xs.reduce(f, a);
// isPrefixOf takes two strings and returns
// true iff the first is a prefix of the second.
// isPrefixOf :: String -> String -> Bool
const isPrefixOf = (xs, ys) =>
ys.startsWith(xs);
// just :: a -> Just a
const just = x => ({
nothing: false,
just: x
});
// log :: a -> IO ()
const log = (...args) =>
console.log(
args
.map(x => JSON.stringify(x, null, 2))
.join(' -> ')
);
// nothing :: () -> Nothing
const nothing = (optionalMsg) => ({
nothing: true,
msg: optionalMsg
});
// readFile :: FilePath -> IO String
const readFile = strPath => {
var error = $(),
str = ObjC.unwrap(
$.NSString.stringWithContentsOfFileEncodingError(
$(strPath)
.stringByStandardizingPath,
$.NSUTF8StringEncoding,
error
)
);
return typeof error.code !== 'string' ? (
str
) : 'Could not read ' + strPath;
};
// show :: Int -> a -> Indented String
// show :: a -> String
const show = (...x) =>
JSON.stringify.apply(
null, x.length > 1 ? [x[1], null, x[0]] : x
);
// takeBaseName :: FilePath -> String
const takeBaseName = strPath =>
strPath !== '' ? (
strPath[strPath.length - 1] !== '/' ? (
strPath.split('/')
.slice(-1)[0].split('.')[0]
) : ''
) : '';
// takeExtension :: FilePath -> String
const takeExtension = strPath => {
const
xs = strPath.split('.'),
lng = xs.length;
return lng > 1 ? (
'.' + xs[lng - 1]
) : '';
};
// File name template -> temporary path
// (Random digit sequence inserted between template base and extension)
// tempFilePath :: String -> IO FilePath
const tempFilePath = template =>
ObjC.unwrap($.NSTemporaryDirectory()) +
takeBaseName(template) + Math.random()
.toString()
.substring(3) + takeExtension(template)
// KEYBOARD MAESTRO ------------------------------------------------------
// macroForUUIDMay :: String -> String -> Maybe Dict
const macroForUUIDMay = strUUID => {
const
js = ObjC.unwrap,
ms = concatMap(
group => {
const mb = foldl(
(m, x) => m.nothing ? (
strUUID === x.UID ? just(x) : m
) : m,
nothing('Not found'),
(group.Macros || [])
);
return mb.nothing ? [] : [mb.just];
}, ObjC.deepUnwrap(
$.NSDictionary.dictionaryWithContentsOfFile(
js(js($.NSFileManager.defaultManager
.URLsForDirectoryInDomains(
14, //$.NSApplicationSupportDirectory,
1 //$.NSUserDomainMask
))[0].path) +
'/Keyboard Maestro/Keyboard Maestro Macros.plist'
)
)
.MacroGroups
);
return ms.length ? (
just(ms[0])
) : nothing('No match found for ' + strUUID);
};
// jsoDoScript :: Object (Dict | Array) -> IO ()
const jsoDoScript = jso => {
const strPath = tempFilePath('tmp.plist');
return (
Application('Keyboard Maestro Engine')
.doScript((
$(Array.isArray(jso) ? jso : [jso])
.writeToFileAtomically(
$(strPath)
.stringByStandardizingPath,
true
),
readFile(strPath)
)),
true
);
};
// Testing whether this code is executing inside a KM macro,
// and if so, supporting instance-sensitive variable resolutions,
// for example for Local or Instance variables.
// kmInstanceMay :: -> Maybe Keyboard Maestro Instance
const kmInstanceMay = () => {
const oInstance = ObjC.unwrap(
$.NSProcessInfo.processInfo.environment
.objectForKey('KMINSTANCE')
);
return Boolean(oInstance) ? (
just(oInstance)
) : nothing('This code is not running in a KM Macro');
};
// FOR -> MAP ------------------------------------------------------------
// groupMay :: Dict kmMacro -> Maybe Dict kmAction
const groupMay = dct =>
foldl(
(m, dct) => m.nothing ? (
dct.MacroActionType === 'Group' &&
isPrefixOf('Group as MAP', dct.ActionName) ? (
just(dct)
) : m
) : m,
nothing('Group not found'),
dct.Actions
);
// forEachMay :: Dict kmMacro -> Maybe Dict kmAction
const forEachMay = dct =>
foldl(
(m, dct) => m.nothing ? (
dct.MacroActionType === 'For' && !dct.IsActive &&
isPrefixOf('MAP', dct.ActionName) ? (
just(dct)
) : m
) : m,
nothing('For Each template not found'),
dct.Actions
);
// MAP behaviour derived from inActive FOR EACH action template
// mapActionMay :: kmMacro Dict -> kmMacro Dict
const mapActionMay = dctFor => {
const dctMap = dctFor.Actions[0];
return just(Object.assign(
dctFor, {
Actions: [
Object.assign(dctMap, {
'Variable': 'Local_ACCUMULATOR',
'Text': '%Variable%Local_ACCUMULATOR%' +
dctMap.Text + '\n',
'ResultAs': dctMap.Variable
})
],
IsActive: true
}
));
};
// mapEvalnMay :: km Action -> Maybe String
const mapEvalnMay = dctMap => {
const strMapValueName = dctMap.Actions[0].ResultAs;
return jsoDoScript([
dctMap,
{ // Map value returned as lines of text
"Variable": strMapValueName,
"MacroActionType": "SetVariableToText",
"Text": "%Variable%Local_ACCUMULATOR%"
}
]) ? just(
'Map evaluated to: ' +
Application("Keyboard Maestro Engine")
.getvariable(strMapValueName)
) : nothing('Map could not be evaluated');
};
// MAIN ------------------------------------------------------------------
const
mbInstance = kmInstanceMay(),
strUUID = mbInstance.nothing ? (
// NB REPLACE THIS UUID, FOR TESTING THE CODE OUTSIDE AN
// EXECUTE JAVASCRIPT ACTION, WITH THE UUID OF THE HOST MACRRO
// OBTAINED IN THE KM EDITOR WITH (Edit > Copy As > Copy UUID)
'A930D513-B651-4C39-A53F-0E082E569AA0'
// (At run-time the correct UUID is obtained automatically).
) : Application("Keyboard Maestro Engine")
.getvariable('LocalUUID', {
instance: mbInstance.just
}),
mbMapEvaln =
bindMay(
bindMay(
bindMay(
bindMay(
macroForUUIDMay( // Macro in which this is running.
strUUID
),
groupMay // JS grouped with disabled FOR EACH Action
),
forEachMay // For Each template (inactive)
),
mapActionMay // Map action derived from For Each template
),
mapEvalnMay // Success or failure of map evaluation
);
// EXECUTE A THE TRANSLATION OF (DISABLED) 'FOR' ACTION
// INTO A MAP
return show(2,
mbMapEvaln.nothing ? (
mbMapEvaln.msg
) : mbMapEvaln.just
);
})();