Is there a simple way to enable/disable macro by name?

I'd like to make an action like the "Trigger macro by name" but instead of triggering the macro, just toggling the enable for it.

Is there a simple way to do this?

Use the Set Macro or Group Enable action.

Use it twice (one set to β€œEnable”, the other set to β€œDisable”) if you want toggle programmatically, for example, in a switch action.

1 Like

If you mean "so I can search for and select a macro or Group at runtime and enable/disable it" then, AFAIK, there's no simple way.

The not-so-simple way would be to use AppleScript (or JXA) to get macros with asstring, parse the resulting plist to extract the UUID:name pairs for all Groups and macros, and convert that into a one-per-line list of the format UUID__macroName, eg

F9A89CF4-D9C2-4DCD-9F48-B81D6BE5FB78__My Test Macro
C8C55A5C-52FD-4E50-9629-E44BF3C22D95__My Other Macro

You'd then present that list with a "Prompt with List" action, and use the returned UUID in an "Execute an AppleScript" action:

set inst to system attribute "KMINSTANCE"
tell application "Keyboard Maestro Engine"

	set theUUID to getvariable "Local_theUUID" instance inst
		
	do script "<dict>
		<key>Action</key>
		<string>Toggle</string>
		<key>MacroActionType</key>
		<string>SetMacroEnable</string>
		<key>MacroUID</key>
		<string>" & theUUID & "</string>
	</dict>"
	
end tell

...to toggle the state of the chosen macro/Group.

The tricky bit is parsing the plist. It would be better to do it "properly", but I'm not smart enough for that and would instead rely on what I believe is consistent text formatting -- 4 tabs then <key>name< and the next line contains a macro name and the next <key>uid< will be followed by that macro's UUID. Similarly with Macro Groups, but with only 2 tabs.

Edit to add:

If I'm wrong, and there is a simple way -- I'm going to feel a complete prat. But that's never stopped me before and it won't stop me now, so here's a POC of the above:

Runtime Enablement Toggle.kmmacros (5.8 KB)

Image

I say "POC" because "For Each"ing through a plist dump in this way is relatively slow -- proper parsing (by someone who knows what they're doing!) would speed this up a lot, even using JXA and doing the text manipulation as an adjunct to getting the plist would help (AS's lack of regex is a killer in these situations).

But doing it in KM does at least show clearly what's happening...

2 Likes

Super cool! It takes around 8 seconds to run in my environment, but I wonder if that could be sped up.

If not, is this a feature you would consider adding? @peternlewis

What about this AppleScript ?

set kmInst to system attribute "KMINSTANCE"
tell application "Keyboard Maestro Engine"
	set theMacro to getvariable "Local__Macro" instance kmInst
end tell

tell application "Keyboard Maestro"
	set enabled of macro theMacro to false
end tell
1 Like

@Macs_Productivity,

I've been using this for over 5 years. It uses the same components as the @devoy and @Nige_S suggestions.

Enable-Disable Macro.kmmacros (4.6 KB)

Hope it helps,
KC

2 Likes

It will, however, launch the KM Editor if it isn't already running. Not a problem for me since the Editor is always open, but might be for whatever OP's doing.

Both methods also have an issue with duplicate names -- yours will, I believe, always target the first found macro/Group with a matching name (search order is, I think, alphabetically by Group and then within the Group) while mine will target the right macro but with no context provided in the pick list when choosing...

1 Like

Ooh..great point. My editor is also always running, and duplicate macro names are not a problem for me. But a great reminder of our unconscious blinders.

1 Like

Excellent solution. Thanks! @devoy @kcwhat

1 Like

Okay! Easy fix. Change "..the macro name.." to "..the macro id UUID.."

That takes care of duplicate macro names. Still needs to have the editor open, I'm afraid.

1 Like

A variant:

  • Multiple selections allowed in Prompt as List action
  • initial states of listed macros expressed in prefix:
    • [x] Enabled
    • [ ] Disabled


Toggle ENABLED state of macros by name.kmmacros (8.5 KB)


Name-sorted listing of macros with uuid and enabled status:

Expand disclosure triangle to view JS source
(() => {
    "use strict";

    const
        onText = "[x]",
        offText = "[ ]";

    const main = () => {
        const macros = Application("Keyboard Maestro").macros;

        return sortOn(x => x.name)(
            zipWith3(
                id => name => enabled => ({ id, name, enabled })
            )(
                macros.id()
            )(
                macros.name()
            )(
                macros.enabled()
            )
        )
            .map(x => `${x.id}__${stateChar(x.enabled)}\t${x.name}`)
            .join("\n")

    };

    const stateChar = enabled =>
        enabled
            ? onText
            : offText;

    // --------------------- GENERIC ---------------------

    // 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;
        };


    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = f =>
        // A copy of xs sorted by the comparator function f.
        xs => [...xs].sort(
            (a, b) => f(a)(b)
        );


    // sortOn :: Ord b => (a -> b) -> [a] -> [a]
    const sortOn = f =>
        // Equivalent to sortBy(comparing(f)), but with f(x)
        // evaluated only once for each x in xs.
        // ('Schwartzian' decorate-sort-undecorate).
        xs => sortBy(
            comparing(x => x[0])
        )(
            xs.map(x => [f(x), x])
        )
            .map(x => x[1]);


    // zipWith3 :: (a -> b -> c -> d) ->
    // [a] -> [b] -> [c] -> [d]
    const zipWith3 = f =>
        xs => ys => zs => Array.from({
            length: Math.min(
                ...[xs, ys, zs].map(x => x.length)
            )
        }, (_, i) => f(xs[i])(ys[i])(zs[i]));


    // 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 main();
})();

Enabled status updating, and listing of results:

Expand disclosure triangle to view JS source
return (() => {
    "use strict";

    const
        onText = "[x]",
        offText = "[ ]";

    const main = () => {
        const
            allMacros = Application("Keyboard Maestro").macros,
            chosenMacros = kmvar
                .local_Choices
                .trim()
                .split("\n")
                .map(x => allMacros.byId(x));

        return either(
            () => ""
        )(
            report => report
        )(
            fmapLR(newState => {
                const prefix = newState
                    ? onText
                    : offText;

                return chosenMacros
                    .map(
                        x => (
                            // EFFECT,
                            x.enabled = newState,

                            // VALUE.
                            `${prefix}  ${x.name()}`
                        )
                    )
                    .join("\n");
            })(
                0 < chosenMacros.length
                    ? Right(
                        !chosenMacros[0].enabled()
                    )
                    : Left("No macros chosen.")
            )
        );
    };


    // --------------------- 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
    });


    // 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);


    // 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));

    // MAIN ---
    return main();
})();
3 Likes

I'm coming to this topic very late. (Real life intrudes.)

In an app with tons of friendly and thoughtful features (and a forum to match!), the getmacros command for an in-memory plist is one of KM's more forbidding. Moreover, it also has a bit of a lopsided effort-reward balance. Here, the advantage of not launching the Editor just to toggle groups and macros means frogging with an in-memory plist via Apple's Key-Value Coding (KVC) query conventions--an acquired taste in AppleScript, a spectacle of incantatory pomp and circumstance in AppleScriptObjC (nonetheless, the toolset geared for plist wrangling).

There is, however, a minor related feature available in getmacros:
the macro group activation toggle uid. (Also present in the xml of the macro group.)

(Thanks to @Nige_S for showing me the way on this.)

Link

Smart Group Syntax for Keyboard Shortcuts: how to include Macro Group hot keys - Questions & Suggestions - Keyboard Maestro Discourse

Explanation

As I tried to remember how to query this in-memory plist, a prior observation by the ever-sharp eyed and even sharper-witted @Nige_S came to mind--finally--who noticed that some macros in gethotkeys are not macros, but rather macro group hot key entries. That is, for any "macro" entry with a name value suffixed with [Macro Group] and sort value prefixed with ξ Ίξ Ί): macro entry β‰  macro. Of course, I foolishly ignored this until now.

It turns out that for [Macro Group] macro entries in both gethotkeys and in getmacros: 1. uid β‰  macro id 2. uid β‰  group id. Rather, uid is the value of the ToggleMacroUID key in the xml of the macro group in Editor.

Pass this uid as a parameter to the "do script" Engine AppleScript and, voilΓ , it will toggle the active state of any macro group that is set to other that "Always Available".

In the dynamic gethotkeys listings, the [Macro Group] entry vanishes when the macro group--its hot keys--becomes unavailable.

The entry is a bit less dynamic in getmacros. It is present even when the macro group is unavailable, but vanishes when the macro goup is disabled.

Since the "uid" (ie.,ToggleMacroUID) won't be valid when the macro group is disabled, this makes sense.

The downside is that getmacros can't be used for getting a comprehensive listing of macro groups with hot keys and their symbols...but that is another discussion.
Smart Group Syntax for Keyboard Shortcuts: how to include Macro Group hot keys - Questions & Suggestions - Keyboard Maestro Discourse

Here is a macro that toggles the enabled state of macro groups and macros or the activation state of macro groups that are enabled and the setting is other that "Always Activated".

Toggle enable or activate from Prompt List of macros or groups.kmmacros (12.7 KB)

Macro Image

USAGE:

Enable the macro and run.

Select a name of macro or group from the Prompt with List.

A macro group or macro selection toggles its enabled state.

A toggle group activation selection toggles the activation state of the group. Palettes will show or hide.

Entries are prefixed with:

  • (MG) = macro group
  • (TG) = toggle group activation state

(I didn't try to remove the trailing [Macro Group] from toggle activation group entries.)

A trailing (x) shows that the group or macro is disabled.

A notification will display either:

  • :radio_button: = toggled activation or
  • :white_check_mark: =enabled or
  • :x: =disabled

followed by (G) or (M) and then the name of the group or macro.

AppleScript

Generates prompt list source (uid__group|macro name):

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions
property author : "@CRLF"
--┏━━━━━━━━━━━━━━━━━━━━━━━
--┃ PURPPOSE:
--┃ Output "UID__macro|group name"
--┃ prompt for list lines
--┗━━━━━━━━━━━━━━━━━━━━━━━

tell application id "com.stairways.keyboardmaestro.engine" to set groups to current application's NSPropertyListSerialization's propertyListWithData:((current application's NSArray's arrayWithObject:(getmacros))'s firstObject()'s |data|()) options:0 format:(missing value) |error|:(missing value)
set macros to (groups's valueForKeyPath:"@unionOfArrays.macros")

set m to macros's filteredArrayUsingPredicate:(current application's NSPredicate's predicateWithFormat:"NOT (%K BEGINSWITH %@)" argumentArray:{"sort", "ξ Ίξ Ί)"})

set gtog to macros's filteredArrayUsingPredicate:(current application's NSPredicate's predicateWithFormat:"%K BEGINSWITH %@" argumentArray:{"sort", "ξ Ίξ Ί)"})

set {genabled, gnames, guids} to (groups's dictionaryWithValuesForKeys:{"enabled", "name", "uid"})'s objectsForKeys:{"enabled", "name", "uid"} notFoundMarker:"not found"

set {menabled, mnames, muids} to (m's dictionaryWithValuesForKeys:{"enabled", "name", "uid"})'s objectsForKeys:{"enabled", "name", "uid"} notFoundMarker:"not found"

set {gtenabled, gtnames, gtuids} to (gtog's dictionaryWithValuesForKeys:{"enabled", "name", "uid"})'s objectsForKeys:{"enabled", "name", "uid"} notFoundMarker:"not found"

set promptArray to current application's NSMutableArray's array()
set disabledSymbol to " (x)"
set i to 0
repeat with aUID in guids
	set enabledSymbol to ""
	if not (genabled's objectAtIndex:i) then set enabledSymbol to disabledSymbol
	set theLine to (current application's NSString's stringWithFormat_("%@__%@ %@ %@", aUID, "(G)", (gnames's objectAtIndex:i), enabledSymbol))
	(promptArray's addObject:theLine)
	set i to i + 1
end repeat

set i to 0
repeat with aUID in muids
	set enabledSymbol to ""
	if not (menabled's objectAtIndex:i) then set enabledSymbol to disabledSymbol
	set theLine to (current application's NSString's stringWithFormat_("%@__%@%@", aUID, (mnames's objectAtIndex:i), enabledSymbol))
	(promptArray's addObject:theLine)
	set i to i + 1
end repeat

set i to 0
repeat with aUID in gtuids
	set enabledSymbol to ""
	if not (gtenabled's objectAtIndex:i) then set enabledSymbol to disabledSymbol
	set theLine to (current application's NSString's stringWithFormat_("ξ Ί%@__%@ %@ %@", aUID, "(TG)", (gtnames's objectAtIndex:i), enabledSymbol))
	(promptArray's addObject:theLine)
	set i to i + 1
end repeat
return (promptArray's componentsJoinedByString:linefeed) as text

Toggles enable of macro or group or activation state of group with uid output.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

--┏━━━━━━━━━━━━━━━━━━━━━━━
--┃ PURPPOSE:
--┃ Process a uid to 
--┃ toggle enable of macro or group or 
--┃ activation state of group
--┗━━━━━━━━━━━━━━━━━━━━━━━

tell application id "com.stairways.keyboardmaestro.engine"
	set theUID to getvariable "localUID" instance system attribute "KMINSTANCE"
end tell

if theUID is not "" then
	if theUID does not start with "ξ Ί" then
		set toggleEnableXML to Β¬
			"<dict>
				           <key>Action</key>
				           <string>Toggle</string>
				           <key>MacroActionType</key>
				           <string>SetMacroEnable</string>
				           <key>MacroUID</key>
				           <string>" & theUID & "</string>
				       </dict>"
		tell application id "com.stairways.keyboardmaestro.engine"
			do script toggleEnableXML
		end tell
	else
		set theUID to text 2 thru length of theUID
		tell application id "com.stairways.keyboardmaestro.engine"
			do script theUID
		end tell
	end if
	set theResults to enabledStatusOfMacroOrGroupUID(theUID)
	if theResults β‰  missing value then
		set {objectType, objectName, enabledStatus} to theResults
		if enabledStatus is "true" then set enabledSymbol to "βœ…"
		if enabledStatus is "false" then set enabledSymbol to "❌"
		if enabledStatus is "toggled" then set enabledSymbol to "πŸ”˜"
		if objectType is "group" then set typeSymbol to "(G)"
		if objectType is "macro" then set typeSymbol to "(M)"
		display notification typeSymbol & space & enabledSymbol & space & objectName
	end if
	return
end if
on enabledStatusOfMacroOrGroupUID(theUID)
	
	tell application id "com.stairways.keyboardmaestro.engine" to set groups to current application's NSPropertyListSerialization's propertyListWithData:((current application's NSArray's arrayWithObject:(getmacros))'s firstObject()'s |data|()) options:0 format:(missing value) |error|:(missing value)
	set macros to (groups's valueForKeyPath:"@unionOfArrays.macros")
	set matches to macros's filteredArrayUsingPredicate:(current application's NSPredicate's predicateWithFormat:"%K == %@" argumentArray:{"uid", theUID})
	set m to matches's firstObject()
	if m β‰  missing value then
		if (m's objectForKey:"sort")'s hasPrefix:"ξ Ίξ Ί)" then
			set theObject to "group"
			set enabledStatus to "toggled"
		else
			set theObject to "macro"
			set enabledStatus to ((m's objectForKey:"enabled") as boolean) as text
		end if
		return {theObject, (m's objectForKey:"name") as text, enabledStatus}
	else
		set matches to groups's filteredArrayUsingPredicate:(current application's NSPredicate's predicateWithFormat:"%K == %@" argumentArray:{"uid", theUID})
		set g to matches's firstObject()
		if g β‰  missing value then
			set theObject to "group"
			set enabledStatus to ((g's objectForKey:"enabled") as boolean) as text
			return {theObject, (g's objectForKey:"name") as text, enabledStatus}
		end if
	end if
	return missing value
end enabledStatusOfMacroOrGroupUID

If anyone wants to read these in-memory plists using KVC, just ask. I'll try to untangle some of the underbrush of the interface the best I can. :smiling_face:

4 Likes

Awesome work!

2 Likes