Greetings all,
I've written a filter utility handler that creates a queryable index of actions from the flat list of actions produced by the recursive handler by Kevin Funderburg.
It leaves the original handler unmodified.
It has one wrapper handler
Input is a list of actions (from the recursion handler) and a boolean query.
For example:
"MacroActionType == 'ExecuteMacro'"
"ActionName CONTAINS 'Test'"
It returns a list of actions matching a query.
Yes, helper handlers contain AppleScriptObjC.
They should not have to be modfied.
The recursion handler should not have to be modified.
Hopefully, the ability to run structured filtering on nested actions is worth dealing with the boolean syntax. (The top hits of the web search of "nspredicate cheatsheet" include a very usable cheatsheet from Dash.)
Here is a macro that filters actions first by MacroActionType. A second pass filters if their target MacroUID is IN, NOT IN the current group's list of macro id.
The color of actions from the results of the first set are set to "Green"
The color of actions from the results of the first set are set to "Red"
AppleScript
-- https://github.com/kevin-funderburg/AppleScripts/blob/master/Keyboard-Maestro/Recursively-Get-Every-Action.applescript
-- https://forum.keyboardmaestro.com/t/feature-request-to-include-the-macro-group-of-the-target-macro-besides-its-name-in-the-execute-macro-macro/10885/15
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions
property author : "@CRLF"
-- ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-- ┃ ACTION FILTERING UTILITY FOR: Recursively-Get-Every-Action.applescript by Kevin Funderburg
-- ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-- ┃ These handlers assume you already have a FLAT list
-- ┃ of KM action references (e.g. from the recursive
-- ┃ getAllActions() routine that gathers nested actions).
-- | To filter the actions use:
-- ┃ actionsMatchingPredicate( query, <list of actions>) for filtering
-- | To filter filtered actions use:
-- | filterActionsByPredicate(query,<list of actions>)
-- | Handlers used by handlers:
-- ┃ Gets array of dictionaries: Use deserializedDictsFromActions(<list of actions>)
-- ┃ Filters the array and gets indexes of actions: indexesMatchingPredicate(query, <array of dictionaries>)
-- ┃
-- ┃ Predicate Format Examples:
-- ┃ "MacroActionType == 'ExecuteMacro'"
-- ┃ "ActionName CONTAINS 'Clipboard'"
-- ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
global allActions
set allActions to {}
tell application "Keyboard Maestro"
set theMacro to item 1 of (selected macros as list)
set groupMacrosIds to id of macros of (macro group of theMacro)
set groupID to id of (macro group of theMacro)
set prefilteredActions to (get every action of theMacro whose xml contains ">ExecuteSubroutine<" or xml contains ">ExecuteMacro<")
my getAllActions(prefilteredActions)
end tell
--Example Usage:
-- Primary filter: match actions by type
set execActions to actionsMatchingPredicate("MacroActionType == 'ExecuteMacro' or MacroActionType == 'ExecuteSubroutine'", allActions)
-- Secondary filter: match a subset in the types of actions found
set groupMacrosIds to listToLiteral(groupMacrosIds) --{"a"}-->"{\"a\"}"
set greenActions to filterActionsByPredicate("MacroUID IN " & groupMacrosIds, execActions)
set redActions to filterActionsByPredicate("NOT MacroUID IN " & groupMacrosIds, execActions)
if (count of execActions) > 0 then
tell application id "com.stairways.keyboardmaestro.editor"
repeat with greenAction in greenActions
set color of greenAction to "Green"
end repeat
repeat with redAction in redActions
set color of redAction to "Red"
end repeat
end tell
end if
--===================RECURSION HANDLER=====================================
-- this subroutine will get every action of the front macro, even those nested
-- within control statements and groups
on getAllActions(actionList)
local actionList
tell application "Keyboard Maestro"
get class of actionList
if (class of actionList = list or ¬
class of actionList = action list) and ¬
(count of items of actionList) > 0 then
repeat with act in actionList
my getAllActions(act)
end repeat
else if class of actionList = case entry then
if (count of actionList's actions) > 0 then
my getAllActions(actionList's actions)
end if
else if class of actionList = action then
--set end of allActions to actionList
set end of allActions to a reference to contents of actionList
-- groups
try
if (count of actionList's actions) > 0 then
my getAllActions(actionList's actions)
end if
end try
-- switch statements
try
if (count of actionList's case entries) > 0 then
my getAllActions(actionList's case entries)
end if
end try
--if then actions
try
if actionList's thenactions ≠ missing value then
my getAllActions(actionList's thenactions's actions)
end if
end try
-- if else actions
try
if actionList's elseactions ≠ missing value then
my getAllActions(actionList's elseactions's actions)
end if
end try
-- try actions
try
if actionList's tryactions ≠ missing value then
my getAllActions(actionList's tryactions's actions)
end if
end try
-- catch actions
try
if actionList's catchactions ≠ missing value then
my getAllActions(actionList's catchactions's actions)
end if
end try
end if
end tell
end getAllActions
--=============================================
-- HANDLERS FOR FILTERING FLAT LIST OF ACTIONS
--=================================================
-- Helper Handler 1: creates a queryable index of actions
on deserializedDictsFromActions(actionsList)
-- Convert AppleScript action references into NSDictionary objects via property list deserialization
set plistXMLList to {} as list
tell application id "com.stairways.keyboardmaestro.editor"
repeat with anAction in actionsList
set xmlString to (xml of anAction)
set end of plistXMLList to xmlString
end repeat
end tell
set plistDicts to (current application's NSArray's arrayWithArray:plistXMLList)'s valueForKeyPath:"propertyList"
return plistDicts
end deserializedDictsFromActions
-- Helper Handler 2 : queries the index and returns the found indices
on indexesMatchingPredicate(predicateString, dictList)
-- Takes a list of NSDictionary objects and returns 1-based indexes matching the NSPredicate
try
set thePredicate to current application's NSPredicate's predicateWithFormat:predicateString
on error errMsg number errNum
return {false, "⚠️ Predicate error: " & errMsg}
end try
set nsDictArray to current application's NSArray's arrayWithArray:dictList
set filteredDicts to nsDictArray's filteredArrayUsingPredicate:thePredicate
set matchIndices to {} as list
-- Iterate over matched dictionaries to find their original positions
set matchCount to filteredDicts's |count|()
repeat with j from 0 to (matchCount - 1)
set aDict to (filteredDicts's objectAtIndex:j)
set zeroIdx to (nsDictArray's indexOfObjectIdenticalTo:aDict) as integer
-- Convert to AppleScript 1-based index
set oneBasedIdx to zeroIdx + 1
set end of matchIndices to oneBasedIdx
end repeat
return matchIndices
end indexesMatchingPredicate
-- Wrapper:Returns a list of actions matching the query directly
on actionsMatchingPredicate(predicateString, actionsList)
-- Returns actions matching the predicate directly
set dicts to deserializedDictsFromActions(actionsList)
set resultOrError to indexesMatchingPredicate(predicateString, dicts)
if class of resultOrError is list and (count of resultOrError) > 0 then
if item 1 of resultOrError is false then
display dialog (item 2 of resultOrError)
return {} -- safe fallback
end if
end if
set idxList to resultOrError
set matchedActions to {} as list
repeat with k in idxList
set end of matchedActions to item k of actionsList
end repeat
return matchedActions
end actionsMatchingPredicate
on filterActionsByPredicate(predicateString, actionsSubset)
set dicts to deserializedDictsFromActions(actionsSubset)
try
set idxList to indexesMatchingPredicate(predicateString, dicts)
on error errMsg number errNum
log "⚠️ Predicate error: " & errMsg
return {}
end try
set filteredSubset to {}
repeat with k in idxList
set end of filteredSubset to item k of actionsSubset
end repeat
return filteredSubset
end filterActionsByPredicate
--Converts a list to a query-ready argument
on listToLiteral(anAppleScriptList)
return (current application's NSExpression's expressionWithFormat:"%@" argumentArray:{anAppleScriptList})'s |description|() as text
end listToLiteral
Explanation
"Actions" is not a true list.
It is more of a plural object, a predecessor to a plist object.
actions whose name contains "Test"
That filters actions matching the actions containing "Test"
{action 1, action 2} whose name contains "Test"
That errors.
What the recursion handler does is give a us flat list of actions--with an xml property.
With this we can emulate the plural object, actions.
What's the catch? Boolean querying.
Instead of:
actions whose name contains "Test"
We use:
"ActionName contains "Test"
BUT...we also can include xml keys in the query.
"MacroActionType == 'ExecuteMacro' OR "MacroActionType == 'ExecuteSubroutine'"
So what? That only gets us the dictionary form of the km action, not the km action itself.
The indices of the list of dictionaries match the list of actions.
It is effectively a queryable index for the actions.
Update
Mofified recursion handler to process try/catch actions