Smart Group Syntax for Keyboard Shortcuts: how to include Macro Group hot keys

I reviewed the smart group search syntax in KM Wiki and can't fine the answer to my question.

i created many smart groups which search for different Keyboard Shortcuts. This allows me to determine which Keyboard Shortcuts are available (ie not being used)

My problem is that I have many Keyboard Shortcuts used as macro group hotkeys (example below) which not displayed in the smart group

thanks in advance for your time and help

I could be wrong, but I don't think it's possible to display groups within smart groups since groups cannot be nested in KM generally. Unfortunately the only workaround coming to mind at the moment would be a bit of nuisance, though maybe there's a way to automate it with applescript? For each group with a palette hotkey you could make a macro in a disabled group (I have one such just for notes) with the same hotkey trigger and then title said macro after the group.


Thank you very much for your suggestion. I think that it's too complicated and would force me to review all my groups anyways. However you have a very good idea: identify all groups with a hotkey. After that I can manage, no problem. I also think that an AppleScript is probably the best solution.

So the question is : how to identify / list all macro groups with a hot key trigger.

thanks again very much for your suggestion.

My trial and erroring comes up with this hypothesis:

  1. Macro groups that do not use hot keys tend not to contain the key "KeyCode" in their activation xml .

  2. Of those that do but still don't use a hot key, the value of KeyCode seems to be 32767.

Why? A LLMV sez: this code point is within the Private Use Area of Unicode (specifically, U+7FFF). Whatever than means.

I've not tested this extensively, but you might see if it serves until something handier/more insightful comes along:

tell application id "com.stairways.keyboardmaestro.editor"
	macro groups whose activation xml contains "KeyCode" and activation xml does not contain "32767"
end tell

Then the question is going to be how to decipher the values of the xml's Modifier and KeyCode keys.

Perhaps someone else knows how to do that.

2 Likes

very interesting ... and very smart. I will see if someone can come up with the final solution. thank you very much !

I can't find a way around this either.

I think so too.

I think,:crossed_fingers:, I have an AppleScript for that.

So it would seem you're now back to the display issue.

A native Smart group display seems out of reach.

A Custom HTML Prompt might be an alternative.

To get you started on how to use the decoding results of the decoding AppleScript, I've uploaded a macro to demo plugging the results into a Custom HTML Prompt.

USAGE:

  1. Enable the macro group and macro
  2. Run the macro.
  3. A list with macro groups and hot keys and enabled state will appear
  4. To edit to the macro group in the KM Editor, click its name
Image

AppleScript Summary
  1. Identify the groups with hot key assignments with the previous AppleScript.

  2. Extract the hot key information from the value of the "group xml" property (deserialized) of the macro groups.

  3. Convert the number values of KeyCode and Modifiers keys to familiar characters and symbols. eg. Modifiers:512 and KeyCode:64 ==> ⌘F7

  4. Format the conversion as preformatted html eg."<a href= "keyboardmaestro://m=" & groupUID & "">" & groupName & " " & modifiersSymbols & keyCodeCharacters

AppleScript
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"
property author : "@CRLF"
--┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
--┃ Outputs information on Macro Groups with hot keys
--┃ in preformatted html. 
--┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
tell application id "com.stairways.keyboardmaestro.editor"
	set groupXMLs to group xml of macro groups whose activation xml contains "KeyCode" and activation xml does not contain "32767"
	set groupDicts to (current application's NSMutableArray's arrayWithArray:groupXMLs)'s valueForKeyPath:"propertyList"
end tell
(*
--To browse all possible info keys, uncomment:
if groupDicts's |count|() > 0 then
	return ((((groupDicts's objectAtIndex:0)'s allKeys())'s sortedArrayUsingSelector:"compare:")'s componentsJoinedByString:linefeed)
end if
*)
set keyCodeDict to keyCodeLookUps()
repeat with aGroup in groupDicts
	set temporaryDict to (aGroup's dictionaryWithValuesForKeys:{"Name", "KeyCode", "Modifiers", "UID"})'s mutableCopy()
	set {aKeyCode, theModifiers} to (temporaryDict's objectsForKeys:{"KeyCode", "Modifiers"} notFoundMarker:"not found")
	set keyCodeCharacters to (keyCodeDict's objectForKey:(aKeyCode)) as text
	set modifiersSymbols to (modifiersToSymbols((theModifiers) as text))
	set modifiersSymbols to reorderModifiersKMStyle(modifiersSymbols)
	(aGroup's addEntriesFromDictionary:(current application's NSDictionary's dictionaryWithObjects:{keyCodeCharacters, modifiersSymbols} forKeys:{"KeyCodeCharacters", "ModifiersSymbols"}))
end repeat

set hrefLines to {}


repeat with aGroup in groupDicts
	
	(((aGroup's allKeys())'s sortedArrayUsingSelector:"compare:")'s componentsJoinedByString:linefeed)
	set enabledStatus to ""
	set {keyCodeCharacters, modifiersSymbols, groupName, groupUID, enabled} to (aGroup's objectsForKeys:{"KeyCodeCharacters", "ModifiersSymbols", "Name", "UID", "IsActive"} notFoundMarker:true) as list
	if not enabled then set enabledStatus to " (disabled)"
	set href to "<a href= \"keyboardmaestro://m=" & groupUID & "\">" & groupName & "</a> " & modifiersSymbols & keyCodeCharacters & enabledStatus
	set end of hrefLines to href
end repeat
set beginning of hrefLines to "<pre>"
set end of hrefLines to "</pre>"
set {TID, text item delimiters} to {text item delimiters, linefeed}
return (hrefLines as text)
--===============HANDLERS=================
on keyCodeLookUps()
	set allObjects to {"A", "S", "D", "F", "H", "G", "Z", "X", "C", "V", "B", "Q", "W", "E", "R", "Y", "T", "1", "2", "3", "4", "6", "5", "=", "9", "7", "-", "8", "0", "]", "O", "U", "[", "I", "P", "Return", "L", "J", "'", "K", ";", "\\", ",", "/", "N", "M", ".", "Tab", "Space", "`", "Delete", "Enter", "Escape", "RightCommand", "Command", "Shift", "CapsLock", "Option", "Control", "RightShift", "RightOption", "RightControl", "Function", "F17", "Key Pad .", "Key Pad *", "Key Pad +", "Clear", "Key Pad /", "Enter", "Key Pad -", "F18", "F19", "Key Pad =", "Key Pad 0", "Key Pad 1", "Key Pad 2", "Key Pad 3", "Key Pad 4", "Key Pad 5", "Key Pad 6", "Key Pad 7", "F20", "Key Pad 8", "Key Pad 9", "F5", "F6", "F7", "F3", "F8", "F9", "F11", "F13", "F16", "F14", "F10", "F12", "F15", "Help", "Home", "PageUp", "Forward Delete", "F4", "End", "F2", "Page Down", "F1", "LeftArrow", "RightArrow", "DownArrow", "UpArrow"}
	set allKeys to {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 67, 69, 71, 75, 76, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 96, 97, 98, 99, 100, 101, 103, 105, 106, 107, 109, 111, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126}
	return current application's NSDictionary's dictionaryWithObjects:allObjects forKeys:allKeys
end keyCodeLookUps

on modifiersToSymbols(bitmask)
	set modifierMap to {¬
		{"⌘", 256}, ¬
		{"⇧", 512}, ¬
		{"⌥", 2048}, ¬
		{"⌃", 4096}, ¬
		{"⇪", 1024}}
	set detected to {}
	repeat with i from 1 to count of modifierMap
		set {symbol, value} to item i of modifierMap
		if (bitmask div value) mod 2 is 1 then
			set end of detected to symbol
		end if
	end repeat
	return detected
end modifiersToSymbols

on reorderModifiersKMStyle(symbolString)
	-- KM-style display order
	set kmOrder to {"⌃", "⌥", "⇧", "⌘", "⇪", "Fn"}
	set reordered to {}
	repeat with s in kmOrder
		if symbolString contains (s as string) then set end of reordered to contents of s
	end repeat
	return reordered as text
end reorderModifiersKMStyle

Display Macro Group Hot Keys Usage.kmmacros (6.9 KB)

2 Likes

I am extremely grateful for your efforts. Is there any risk in running this AppleScript ? thank you very much

@ronald

Your concern is understandable and I apologize for not stating it up front clearly: the script is read-only. It reads the read-only "group xml" property of the AppleScript macro group. So no, no more risk than any other AppleScript that reads "macro group" properties, like: name of macro group 1

This line in particular may be what has you concerned:

(aGroup's addEntriesFromDictionary:(current application's NSDictionary's dictionaryWithObjects:{keyCodeCharacters, modifiersSymbols} forKeys:{"KeyCodeCharacters", "ModifiersSymbols"}))

It may, at first glance, appear to be writing to a live macro group, but it is not writing to an AppleScript macro group, but rather to a copy of the the macro groups' xml that has been deserialized, here:

tell application id "com.stairways.keyboardmaestro.editor"
	set groupXMLs to group xml of macro groups whose activation xml contains "KeyCode" and activation xml does not contain "32767"
	set groupDicts to (current application's NSMutableArray's arrayWithArray:groupXMLs)'s valueForKeyPath:"propertyList"
end tell

Any reading or writing is done to those objects, which have no more connection to the live "macro group" than any other variable that receives a macro group property value.

What you're seeing in the repeat loop is an NSArray of NSDictionaries (the deserialized group xml) being acccessed, that mirrors the list of macro groups. NSArray is corresponds to list. NSDictionary corresponds to a macro group. We do this because a AppleScript "macro group" does not have AppleScript properties "key code" or "modifiers", so we have to work with the keys and values in the macro group's "group xml" property to get our info.

Deserialization simply means putting the raw xml--which has no interface at all--of a macro group into an NSDictionary object, which does have a read-write interface, so that we can retrieve the KeyCode and Modifiers value in a structured way.

Again, that object is only an object variable disconnected from the macro group.

This is the thing to look for when you are trying to see write to the macro group: set activation xml of macro group 1 to someXML.

Also note, the property "group xml" is not settable/writable.

Here's a screenshot of macro group AppleScript properties. "get/set" indicates a writable property. If there is no "set", the property is read-only.

The reason I wrote to the dictionary object iteself, rather than a subset dictionary, is to give you the option to explore accessing other info key values such as:

Activate
AddToMacroPalette
AddToStatusMenu
CreationDate
CustomIconData
DisplayToggle
IsActive
KeyCode
Modifiers
Name
PaletteUnderMouse
Targeting
Theme
ToggleMacroUID
UID

Thank you for the great question and the chance to clear it up.:pray::grinning:

I hope the script works for you!

Additional clarifiation.

Of course. :man_facepalming:t2: The Keyboard Maestro URL schemes used in the html of the Custom HTML Prompt action: keyboardmaestro://m=

This page explains explains: "Edit a specific macro or macro group." This means clicking on the link makes the Keyboard Maestro Editor go to the macro or macro group in Edit mode.

https://wiki.keyboardmaestro.com/manual/URL_Schemes

You script is astounding. I am speechless. A work of art. I should pay an annual subscription fee to use it. I fell off my chair. Exactly what I was looking for and .. even more in an elegant HTML window. And thanks very much for your detailed explanation which reassured me. I hesitated before asking my question because I did not want to offend you, so I am relieved by your understanding. thank you so much !!

1 Like

The best reward of all.

If so, I am doubly happy to have the feedback that it didn't seize up and fail!:rofl:

Wow. If you say so...:wink:

That is embarrassing...ask anything.

What I really don't want is lingering doubts.

Thank you for the interesting topic!

2 Likes

One word of caution -- keyCodeLookups() is specific to a particular keyboard layout. Non-English keyboard users will need to roll their own character list, perhaps using the graphic on Complete list of AppleScript key codes to match key position to code.

1 Like

@Nige_S

Absolutely.

Thank you for taking the time to post an explicit caveat and the link. :pray: Another even more serious omission and on my part.

Big apologies for not stating up front that this macro's key mapping is for an English language keyboard only. This macro cannot in anyway hope to replicate the accuracy of what the trigger display that the user sees in KM Editor. The value of KeyCode comes from Keyboard Maestro. The character representation from the macro is completely my mapping, which, hope (not much of a guarantee), is accurate for English language keyboards.

Having said that, be sure to check the macro's representation against what the macro groups' trigger displays.

trigger display

There must be some way of going from key code to current keyboard langauge character -- after all, the KM Editor does it!

You can't do it in "normal" AppleScript, but perhaps there's something in ObjC? Something I know next to nothing about...

Meanwhile something to consider -- and a placeholder for me tomorrow, if nobody picks it up before then:

These "Macro Group hot keys" are, presumably, just hot keys like those that trigger macros. So they should be listed in the output of

tell application "Keyboard Maestro Engine" to gethotkeys with asstring

...and, at a quick look, they seem to be distinguished by having " [Macro Group]" appended to their name, eg

				<key>name</key>
				<string>TextEdit [Macro Group]</string>

Going that route will have a couple of benefits:

  1. The hot key's textual representation, including current-language character for the key code, is included in the returned XML, eg <string>⌃⌥⌘Z</string>
  2. Only the Groups active in the current context will be included -- and context is important if you are looking for actual free or clashing key combos
2 Likes

Thank you for the brainstorming! I'd all but resigned to using look up dictionaries.

AppleScriptObjC can't I am pretty sure.

JXA might have a slightly better chance, but I think also: no.

Straight Objective-C most certainly can, but AppleScriptObjC is as close as it comes to Execute an Objective-C code. Objective-C means a compiled cli tool download. AppleScriptObjC alone is enough to give people, myself included, the whim-whams on download.

Swift will surely have an equivalent... Uh-oh, I'm getting myself in over my head. :wink: This seems to happen when @Nige_S enters the discussion. :joy:

I agree. The numeric value of KeyCode key in the "group xml" is exactly the same as KeyCode in the xml of trigger of macro. It would be the bees' knees to have the character/symbolic representation, but KeyCode's number is all I could come up with for hot keys for Macro Groups.

As far as I understand it, KM Engine's gethotkeys' group keys are limited to name,sort, uid.

KM Engine's getmacros ditto.

Maybe, there is some sort of plist conversion table squirrelled in the KM or KME app that can be used. Will forage tomorrow.

Swift may be the only game left.

Then, it almost seems that the whole AppleScript would need to be rewritten in Swift.

Theoretically possible hearsay has it....

Or... maybe use the Swift from the AppleScript:

tell application "Keyboard Maestro Engine" to set keyCodeCharacter to do script "Swift Utility Macro" with parameter KeyCode

And via a Return action return it to AppleScript.

It might not be horribly slow.

1 Like

The Sauce package might help here, and it looks like you can use Packages with Swift scripts. But I am not a Swiftie -- in any sense of the word :wink: -- so even those links take me way out of my depth...

Default keyboard layouts are apparently still defined within /System/Library/Keyboard\ Layouts/AppleKeyboardLayouts.bundle -- but that is SIP-protected, so not an easy nor general solution (assuming we can even make sense of their contents!).

Programmatically generating your own list, using System Events to "type" key codes into TextEdit, is a non-starter -- fun things happen when you start "typing" Fn key codes :wink:

Here goes -- this should display the hot key combo, name, and UUID of every currently active Group with a hot key:

Active Groups with Hot Keys.kmmacros (7.0 KB)

Image

Text parsing is a bit clunky :frowning: Someone who knows JXA and XML could do a much cleaner job. But it does seem to work!

After all that, I do have to ask one big question...

Aside from an understandable desire to catalogue hot keys, why does this matter?

These are just hot keys. They're always "available" -- and if they aren't unique in the current context you'll get a Conflict Palette.

Set your preferred, preferably memorable, hot key combo. Switch to the context in which you'll be using it. Try it. If you don't get a Conflict Palette it's unique -- if you do then you can either change it or just use it the same way as any other Conflicting macro.

2 Likes

yes, you are right. In that case, you could say the same thing about all hotkeys which I classify using smart groups. It's just that I like to plan in advance, and find it tedious to have to test all with conflict palettes. That being said, bottom line, you are right.

Do I understand that your macro is keyboard independent ?

thank you

Yes, but it only accounts for Macro Groups available in the testing context.

I found that got very complicated as soon as any context availability was set on any Macro Group. My plans rapidly filled with either false conflicts or, when the one I wanted to set was globally available, false availability in certain contexts.

Someone better organised than me could probably manage the multiple tables involved -- but I'm afraid that I gave up!

1 Like

You are fantastic and I am extremely grateful !!

thank you again very much for the macro.

Yes! This completely escaped my notice. Thank you!

Thanks for the links! I know just enough to get me into trouble...

So, though we can't use Smart Groups to view macro groups's hot keys, we have a few lookup options avaiable:

  1. KM Engine's gethotkeys does indeed provide hot key info not only for active macros but active macro groups. The info available by filtering on names of macros that contain "[Macro Groups]". (Unfortunately, "gethotkeys with getall" provides only name, uid, sort info, so no comprehensive info.) As far as I can seen KM Engine's getmacros does not offer hotkey info for macro groups, only macros.

The group xml of AppleScript macro group provides numeric info in the keys, KeyCode and Modifiers (same as xml of macro).

A couple of ways to convert these to symbols and characters.

  1. A Swift look up, which still needs handmade symbols for things like FKeys, keypad symbols, Home,End,arrow up,down,left,right, page up,page down forward delete Enter and others. It is in a separate macro,"Character from Carbon Key Code via Swift", that the AppleScript uses from with KM Engine's do script.

  2. A hand made look up table (in this macro only for English language keyboard).

Below is a revised version of the originally posted macro that offers 3 different look up modes corresponding to these 3 different strategies:

Specify a value in the localLookUpMode variable

  1. active groups
  2. all groups
  3. all groups english keyboard

Run the macro with a trigger of your choice

The macro will display names and hotkey information for macro groups with hot keys.

  1. Active groups only
  2. All groups including disabled info
  3. All groups including disabled info. Only accurate for English language keyboards.
1. gethotkeys AppleScript
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions
hotKeyReportFromActiveGroupsWithHotKeys()
on hotKeyReportFromActiveGroupsWithHotKeys() --via gethotkeys
	tell application id "com.stairways.keyboardmaestro.engine" to set groups to current application's NSPropertyListSerialization's propertyListWithData:((current application's NSArray's arrayWithObject:(gethotkeys))'s firstObject()'s |data|()) options:0 format:(missing value) |error|:(missing value)
	set hk to (groups's valueForKeyPath:"@unionOfArrays.macros")
	set groupHKDicts to hk's filteredArrayUsingPredicate:(current application's NSPredicate's predicateWithFormat:"%K CONTAINS %@" argumentArray:{"name", "[Macro Group]"})
	set hrefLines to {}
	repeat with aGroup in groupHKDicts
		set {hotKey, groupName, groupUID} to (aGroup's objectsForKeys:{"key", "name", "uid"} notFoundMarker:true) as list
		set href to "<a href= \"keyboardmaestro://m=" & groupUID & "\">" & groupName & "</a> " & hotKey
		set end of hrefLines to href
	end repeat
	set beginning of hrefLines to "<pre>"
	set end of hrefLines to "</pre>"
	set {TID, text item delimiters} to {text item delimiters, linefeed}
	return (hrefLines as text)
end hotKeyReportFromActiveGroupsWithHotKeys
  1. is the lookup via Swift. It is something toggled together with an LLM. Interesting as proof of concept, but slow. Its output should be keyboard independent.
2. Swift code
import Foundation
import Carbon

// Fallback symbolic representation for non-printing keys
func fallbackSymbol(for keyCode: UInt16) -> String {
    switch keyCode {
    // Common control keys
    case 36:  return "⏎"    // Return
    case 76:  return "⌤"    // Enter
    case 48:  return "⇥"    // Tab
    case 51:  return "⌫"    // Delete (Backspace)
    case 117: return "⌦"    // Forward Delete
    case 53:  return "⎋"    // Escape
    case 71:  return "⌧"    // Clear
    case 114: return "?"    // Help (no symbol defined — using ?)

    // Arrow keys
    case 123: return "←"
    case 124: return "→"
    case 125: return "↓"
    case 126: return "↑"

    // Navigation keys
    case 115: return "↖︎"   // Home
    case 119: return "↘︎"   // End
    case 116: return "⇞"    // Page Up
    case 121: return "⇟"    // Page Down

    // Keypad (⊞ prefix)
    case 65:  return "⊞."   // Keypad .
    case 67:  return "⊞*"   // Keypad *
    case 69:  return "⊞+"   // Keypad +
    case 75:  return "⊞/"   // Keypad /
    case 78:  return "⊞-"   // Keypad -
    case 81:  return "⊞="   // Keypad =
    case 82:  return "⊞0"
    case 83:  return "⊞1"
    case 84:  return "⊞2"
    case 85:  return "⊞3"
    case 86:  return "⊞4"
    case 87:  return "⊞5"
    case 88:  return "⊞6"
    case 89:  return "⊞7"
    case 91:  return "⊞8"
    case 92:  return "⊞9"



    // Function keys
    case 122: return "F1"
    case 120: return "F2"
    case 99:  return "F3"
    case 118: return "F4"
    case 96:  return "F5"
    case 97:  return "F6"
    case 98:  return "F7"
    case 100: return "F8"
    case 101: return "F9"
    case 109: return "F10"
    case 103: return "F11"
    case 111: return "F12"
    case 105: return "F13"
    case 107: return "F14"
    case 113: return "F15"
    case 106: return "F16"
    case 64:  return "F17"
    case 79:  return "F18"
    case 80:  return "F19"
    case 90:  return "F20"

    default:
        return "KeyCode \(keyCode)"
    }
}


// Translate key code to visible character or fallback symbol
func character(for keyCode: UInt16) -> String {
    var deadKeyState: UInt32 = 0
    var chars = [UniChar](repeating: 0, count: 4)
    var length: Int = 0

    guard let inputSource = TISCopyCurrentKeyboardLayoutInputSource()?.takeRetainedValue(),
          let rawLayout = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData) else {
        return fallbackSymbol(for: keyCode)
    }

    let cfData = unsafeBitCast(rawLayout, to: CFData.self)
    let data = cfData as Data

    var result = ""
    data.withUnsafeBytes { buffer in
        guard let base = buffer.baseAddress?.assumingMemoryBound(to: UCKeyboardLayout.self) else { return }

        let status = UCKeyTranslate(
            base,
            keyCode,
            UInt16(kUCKeyActionDown),
            0, // No modifiers
            UInt32(LMGetKbdType()),
            UInt32(kUCKeyTranslateNoDeadKeysBit),
            &deadKeyState,
            chars.count,
            &length,
            &chars
        )

        if status == noErr && length > 0 {
            let s = String(utf16CodeUnits: chars, count: length)
            // Use only visible ASCII (not control chars)
            if s.unicodeScalars.allSatisfy({ (32...126).contains($0.value) }) {
                result = s
            }
        }
    }

    return result.isEmpty ? fallbackSymbol(for: keyCode) : result
}

// Read KM variable and print result
let env = ProcessInfo.processInfo.environment
let codeStr = env["KMVAR_localKeyCode"] ?? ""
guard let keyCode = UInt16(codeStr) else { exit(1) }

print(character(for: keyCode))

All of these modes are read-only.

This list of strategies are hardly exhaustive, I'm sure, but they sure feel that way.:wink:

Display Macro Group Hot Keys Macros.kmmacros (20.1 KB)

1 Like