[Feature Request] To include the Macro Group of the target macro besides its name, in the "Execute Macro" macro

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

Filter Flat list of actions Macros2.kmmacros (30.3 KB)

1 Like