Feature Requests: Comments on Triggers, Rearranging Triggers

After you've copied it:

-- get copied trigger list
set theTriggers to (get the clipboard)

-- remove the icon text
set AppleScript's text item delimiters to "Trigger icon"
set theTriggers to every text item of theTriggers
set AppleScript's text item delimiters to ""
set theTriggers to theTriggers as text

-- add the "Comment" action
tell application "Keyboard Maestro"
	set theXML to "<dict>
		<key>MacroActionType</key>
		<string>Comment</string>
		<key>Text</key>
		<string>" & theTriggers & "</string>
		<key>Title</key>
		<string>Trigger List</string>
	</dict>"
	set theAction to make new action at beginning of macro id (item 1 of (get selectedMacros))
	set xml of theAction to theXML
end tell

...will insert a new "Comment" action at the beginning of the macro, like this:

If you want formatting, icons, etc you will have to jump through some more hoops -- the "Comment:" text in the XML appears to be base64-ed RTFD. At that point it is probably easier to drive the KM Editor UI -- select first action, insert a "Comment", "Move Action Up" then "Enter Action" menu items, set "Title" field, tab, paste in the trigger list.

1 Like

Thanks @Nige_S,

That's awesome!

Another route to formatted text without having to deal with base64 would be to open a temporary Text Edit file, past text, manipulate text, copy text, close file without saving, paste copied text into comment. That has the "drawback" of watching the TE file get opened and closed, but that's an acceptable tradeoff for being able to do the formatting with familiar tools.

I'm amused at your use of

set AppleScript's text item delimiters to "Trigger icon"

to eliminate that text. I've never seen anything quite like it and it's very clever.

When you set it back, are you setting it to the empty string?

Yeah -- I'd class that under "drive the UI". I'm sure there must be a way to keep an un-edited Copy of the triggers as RTFD, but it's beyond me.

RTFD is probably only so you can have the images -- I shall have a play with RTF at some stage.

Yep, we're setting it back to the empty string so there's nothing between the list items when we cast them back to text.

TBH it's a bit broad-brush way of doing it and, given we're KM users, we can do a lot better:

tell application "Keyboard Maestro Engine"
	set theTriggers to (search theTriggers for "(?m)^Trigger icon" replace "" with regex and case sensitive)
end tell

No need for lists at all, much more precise, and probably faster for larger amounts of text.

I could use some help walking through how that magic line works. The beginning:

tell application "Keyboard Maestro Engine"
   set theTriggers to (search theTriggers for ...

seems pretty straightforward. That is it telling KBM to set the variable called theTriggers to the result of the search, where the search is within the existing value of theTriggers. I presume theTriggers is what we got from the clipboard, as previously.

But the rest of the line reads, to my inexperienced eye, a bit inside out and backwards.

Here's my interpretation, based on the goal:

The with at the end contains two parameters, regex and case sensitive, and those specify how the string after the for is interpreted. Whenever, wherever there's a match, the found string gets replaced with "".

How did I do? Have I got that part of it right?

Then after theTriggers is edited, we construct the XML for a new Comment action and add it to the beginning of the first currently selected macro.

Still OK?

Sometimes the fact that AppleScript is supposed to be "English-like" gets in the way of readability, IMHO.

You're there, but for clarity:

tell application "Keyboard Maestro Engine"
   set theTriggers to (...)
end tell

We're telling KM Engine to set the variable theTriggers to the result of the expression inside the brackets. As to what's in there:

search theTriggers for "(?m)^Trigger icon" replace "" with regex and case sensitive

search is a verb in KME's dictionary:

search v : Search a string for a string and replace occurances with another string, returning the resulting string.
   search text : the input string.
      for text : the search string string.
      replace text : the replacement string.
      [regex boolean] : using regular expressions (default false).
      [case sensitive boolean] : case sensitive (default false).
      [process tokens boolean] : process tokens (default false).
      [first boolean] : just first match (default false).
      [last boolean] : just last match (default false).
      → text : the resulting string.

...so it quite literally reads as:

Search the text in theTriggers for "(?m)^Trigger icon", replacing every occurence* with "", treating our search term as a regular expression and making the search case sensitive

*Because we didn't specify first or last

And because we're telling KME and using the KM regex engine we've access to all the things we know and love in the regular expression actions, like the (?m) "multiline" option.

The result of the expression -- the transformed text -- is what gets put into theTriggers.

It's the AppleScripted equivalent of

1 Like

THANKS! I had been intending to ask you how that might be done in KBM instead of AppleScript talking to KBM. I appreciate your anticipating me!

Given that you / I / we can edit the captured list with a KBM action, am I right in assuming that you did it in AppleScript because you were already coding in AppleScript to be able to add a new action, the Comment, to the KBM macro?

I'm thinking I would like to put this all into a KBM macro that would be able to modify the currently selected/edited KBM macro (I would call it by menu, it's not a frequent enough action to get a hotkey). Instead of using AppleScript to tell KBM to define a string of the new XML, would there be a downside to doing it in KBM instead? I.e, that macro would:

  • edit the selected list of triggers in KBM (with the above action)
  • build the XML structure of the new Comment action in KBM as a text variable
  • then run AppleScript only to insert the built XML into the KBM macro.

And as a brainstorm, to close the loop, so to speak, I'm thinking that it might be convenient to be able to take a manually modified update of the Comment text and convert it into the XML Triggers array:

	<key>Triggers</key>
	<array>
		<dict>
			<key>FireType</key>
			<string>Pressed</string>
			<key>KeyCode</key>
			<integer>0</integer>
			<key>MacroTriggerType</key>
			<string>HotKey</string>
			<key>Modifiers</key>
			<integer>0</integer>
		</dict>
		. . .
	</array>

If I understand the process, I think that I would need to do something like:

set allTheTriggers to (get triggers of selectedMacro)
set xml of allTheTriggers to theNewTriggersListInXML

Do I seem to be on the right track? I suppose the details will depend on exactly what I get when I run

print xml of triggers of selectedMacro 

or something like that.

What I'm wondering is if I can replace all the triggers at once using AppleScript this way, or if I need to empty the list and then add triggers one at a time.

I'm probably getting way ahead of myself here, but the prospect of being able to edit a Comment text and update the same macro's trigger list from that is attractive. Probably more work than it's worth, but still attractive for fun of it.

When I define the XML of the Comment to be <key>Text</key>, then I get text in a font that is not the default font for Comments. If I apply a Style to the Clipboard using the Apply Style to System Clipboard action, it makes no difference.

As you hinted at, it would indeed be nice to be able to generate that <data> string from the Styled text in the clipboard.

The <data> string looks like it may be "UUEncoded" binary. If the Styled clipboard is binary, maybe UUEncode could be used to create the <data> string.

@peternlewis -- You know what's under the hood in the StyledText of a Comment. Could the Apply Styles to System Clipboard action be used to generate a source that could then be edcoded as the StyledText entry's <data> element?

No idea.

The data is created from a NSAttributedString with the command:

[s RTFDFromRange:NSMakeRange(0,s.length) documentAttributes:@{}]

Any text encoding is done by the plist encoding.

I've made an upgrade to @Nige_S's macro/AppleScript above.

I figured out how to get all the triggers without having to manually copy the list from KBM's Non-Editing Mode.

Here's my test macro with three triggers and the Comment action automatically inserted into itself:

image

It's based on an AppleScript snippet that I found a Forum entry from Peter Lewis (@peternlewis) that gets a trigger of a macro (Change the Hot Key for Multiple Macros at Once - #7 by peternlewis). I used that as the basis for some AppleScript that loops through all of the triggers individually.

image

-- identify the currently selected macro
tell application "Keyboard Maestro"
	set selectedMacro to macro id (item 1 of (get selectedMacros))
	
	-- get list of triggers
	set theTriggerList to triggers of selectedMacro
	
	-- put description and xml of each into variable
	set theDescriptionList to ""
	
	repeat with theTrigger in theTriggerList
		set theDescriptionList to ¬
			(theDescriptionList & ¬
				(get description of theTrigger) & linefeed)
	end repeat
	
	-- add the "Comment" action
	set theCommentXML to "<dict>
		<key>MacroActionType</key>
		<string>Comment</string>
		<key>Text</key>
		<string>" & theDescriptionList & "</string>
		<key>Title</key>
		<string>Tiggers for This Macro</string>
	</dict>"
	set theAction to make new action at beginning of macro id (item 1 of (get selectedMacros))
	set xml of theAction to theCommentXML
	
end tell

At this point, the extension of this idea that I mentioned above, being able to edit the Comment list and use that to regenerate the macro's triggers, is one step closer — and it's clear I have about 20 steps to go. (I'm not holding my breath.)

There are two "bugs" in the formatting of the above generated Comment text:

  • I can put a blank line above, after, or both, but I haven't been able to figure out how to only have the list text, with no spurious blank line.
  • The list starts out in Helvetica font and whenever a modifier key appears, from there on the text is in Lucida Grande.

I've banged on some ideas to fix both of those, but nothing easy has come to mind. Very much not a big deal.

With your code as written, the easiest way is to trim off the final linefeed after you've built the list with your loop:

set theDescriptionList to (characters 1 thru -2 of theDescriptionList) as text

The only way I've found to control style is to bounce out to HTML, use textutil to convert that to RTF then base64 the result before posting the result back to a KM variable. That's a bit of a faff, but shout if you want a demo.

I'm saying that, rather than just posting, because:

...I hadn't realised this was your ultimate goal. I'm not saying that this is impossible, but get a bunch of triggers as XML and it looks rather difficult. Just looking at hot keys, you're going to have to map letters to key codes, modifiers to their combined integer value, key actions to fire types and tap counts...

Auto-generating a comment with a list of triggers that you can then edit with "and this variation forces branch A" seems reasonable. Re-arranging trigger order could be doable. But creating a trigger list from scratch from a comment's text seems more work than simply editing the triggers in the GUI!

In general, I agree. But I have a situation that is prompting me in the automated direction.

I have a list of triggers, ⌥⌘A through ⌥⌘Z that I originally started with, as potential hotkeys to switch to different desktops. I deliberately left out ⌥⌘Q and ⌥⌘W because of their nearly universal and annoying side effects if they aren't caught by KBM. I also wanted the keys to be mnemonic, so I still haven't used "J" but "K" was one of my first ones, for my KBM desktop. Over time I have been adding shifted versions of them, e.g.,⌥⇧⌘K became my KBM "meta" desk, for Forum research and AppleScript testing.

Because of the nature of how additions are made to the KBM Triggers list, all the shifted triggers are now at the bottom of the list and they are not in alphabetical order. Recently I added ⌥⇧⌘A for a second Admin desktop so now ⌥⌘A is at the top and ⌥⇧⌘A is at the bottom of a list of 36 triggers. My aesthetics rebell. Clean code should be readable, organized, and even beautiful, if possible. To clean it up would mean over ten minutes of tedium, retyping the list in the new order, and future changes would have to be done all over again, so I'm spending up to 10 hours seeing if I can program it, because for me the programming — and the learning that comes along with getting the programming to work — is fun (most of the time).

For instance, to replace the list of triggers, I am pretty sure that I have to first delete the existing list of triggers, one by one. In AppleScript, I got the list of triggers from KBM and stepped through the list, deleting them one at a time. But there was a knotty problem. If I had a test list of four triggers, the loop would delete two of them. If I made a test list of eight triggers, the loop would delete four of them. WTH?

I finally figured out that AppleScript was creating the list of triggers and then stepping through its internal list to execute the deletions. So with four triggers, it deletes trigger 1 and then the list shifts so that when it deletes trigger 2, it's actually deleting trigger 3. Then it attempts to delete trigger 3, but there is no trigger 3, so it quits. In a list of 36 triggers, it would presumably delete every odd trigger until it had deleted 18 of them — not my intent.

I am sure there is a simple AppleScript option/flag to process the list in reverse order, so as soon as I find that, I presume this step will be done and I can move onto the next step.

I think both of those will be simple, although actually coding the dictionary/look-up table for the key codes may take some creative use of VIM and SED or some such.

I am definitely not going to bother with this stuff in the first (or even second) pass.

Well that was quick. Here's the AppleScript that empties the Trigger List, before repopulating it:

-- identify the currently selected macro
tell application "Keyboard Maestro"
	set theSelectedMacro to macro id (item 1 of (get selectedMacros))
	
	-- get list of triggers
	set theTriggerList to triggers of theSelectedMacro
	
	-- for each trigger in the reversed list, delete it.
	repeat with theTrigger in (reverse of theTriggerList)
		delete theTrigger
	end repeat
	
end tell

Totally understand the desire -- the urge, even -- to reorganise.

If all you want to do is reverse the current list, try

tell application "Keyboard Maestro"
	set selectedMacro to macro id (item 1 of (get selectedMacros))
	
	set theTriggers to reverse of (get triggers of selectedMacro)
	repeat with eachItem in theTriggers
		copy eachItem to end of selectedMacro's triggers
	end repeat
	repeat count of theTriggers times
		delete first trigger of selectedMacro
	end repeat
end tell

If you want to sort the triggers, however, you are going to have to determine your desired sort order and then, perhaps, roll your own code for that. As an example of the possible problems -- if you want "alphabetically by the non-modifier key" then even if you only have hot key triggers an ASCII sort won't do it since:

The Hot Key ⌃⌥⌘A is pressed
The Hot Key ⌃⌘B is pressed

...sorts to:

The Hot Key ⌃⌘B is pressed
The Hot Key ⌃⌥⌘A is pressed

No, you just blat the lot!

tell application "Keyboard Maestro"
	set selectedMacro to macro id (item 1 of (get selectedMacros))
	delete every trigger of selectedMacro
end tell

Remember that you are dealing with references to triggers, not the triggers themselves. So when you delete item 1 of the triggers then go to delete item 2, it's item 2 of the current list -- i.e. item 3 of the original. That's why you were nuking every other trigger -- and why, in my script above, I added all the triggers to the end again then nuked the originals.

As usual with me, none of this is gospel. TBH, AS lists and references and so on do my head in, I don't fully understand them, but do know enough to keep poking around until things work :wink: So consider the above as pragmatic solutions/truths that could be improved on by someone who actually knows what they are doing!

1 Like

Thanks for all the detailed help, @Nige_S. I've been approaching the sorting problem in a different thread (How to sort a list by first non-modifier character?) and @ComplexPoint came up with the Shell commands to do that — IF I first insert delimiters around the modifier symbols so that the input line can be broken into distinct fields. It's some pretty basic Regex editing to do that and then remove them again. The tricky part is how to determine where they go if the trigger has no modifiers. But I don't think that's the case in my first usage so I can leave that as a special case to deal with later, if ever.

Well isn't that a whole lot easier!

Unfortunately, that approach doesn't work when...

I do want to sort the triggers, and in the other thread mentioned above I've been getting help working out the sorting order. But to actually sort the triggers themselves, in the macro, I think I need to:

  • Save the XML of each trigger and name it based on the description of the trigger.
  • Generate a list of all the names.
  • Sort the list of names using first whatever follows the modifier symbols (see other thread).
  • Insert the saved trigger XML into the macro in the order determined by the sorted list of names.

Which is why we add the "new" triggers to the end of the trigger list, then delete the "old" ones from the beginning of the list.

Rather than mess around with XML etc, I'd take a similar approach for what you want to do.

  1. Get a list of trigger descriptions
  2. Create a "sorted list", based on description, of trigger indices
  3. Work through that sorted list, appending triggers by index
  4. Delete original triggers

Let me get this straight and clear. You're suggesting that the sorted list doesn't need to point to saved files of the XML for each trigger, or to KBM named clipboards or anything else that saves the XML code for each trigger, it only needs to have the index number of where that entry was/is in the original list. Just the original index number, nothing else.

Then you would go through the sorted list and add new triggers, simply by copying the original trigger by its index number as a new trigger, and do this for the whole sorted list. At this point the trigger list is twice as long as it should be, containing both the unsorted and the sorted triggers, in that order.

Then you delete the first half of the list by deleting items 1 through count, where count is a number you saved somewhere along the way, the size of the original list.

That's pretty cool, and I don't think that I would have thought of that, I was really focussed on how to save the XML to use it to create new triggers after I had deleted the originals. Your method, by keeping the originals around to copy them, is much cleaner and there's no temporary files or named clipboards or whatever to have to go back and clean up.

Thanks.

All that can be done in AppleScript with variations on what I've already done — except the Regex editing and the Shell-based sort. Using the Shell's sort is really convenient because it handles sorting by different fields so easily, but it requires inserting and removing delimiters into the items. I suppose I could pop back to KBM to do that and then go into AppleScript again to finish up.

You wouldn't happen to have slick tricks for how to run a Shell script from inside AppleScript, would you?

This, perhaps, demonstrates what's going on. Run it from your favourite script editor and it will bring KME to the front, then pause for 2 seconds within each loop so you can watch the trigger list changing in the UI:

tell application "Keyboard Maestro"
	activate
	set selectedMacro to macro id (item 1 of (get selectedMacros))
	delay 2
	set theTriggers to reverse of (get triggers of selectedMacro)
	repeat with eachItem in theTriggers
		copy eachItem to end of selectedMacro's triggers
		delay 2
	end repeat
	repeat count of theTriggers times
		delete first trigger of selectedMacro
		delay 2
	end repeat
end tell

One clarification:

count is a property of a list -- the number of items in it. You can also use length, which might be more in line with other languages, but AS or more English-like (you don't "length" the number of things in a box, you "count" them!).

set myList to {"one", "two", "three", "four"}
count of myList
--> 4

So we don't need to save the value, we just ask theTriggers how many items it contains.

Check the do shell script verb in the Standard Additions dictionary. But I think that, in this case, you're better off bouncing to KM, text processing there, then coming back to AS to make the trigger changes.

Don't forget that, as users of both AS and KM, we can call the KME's regex engine with AppleScript!

Also, a length is a measurement, a "real" number with potentially infinite decimals and roundoff error, a count is a whole number, an integer, no rounding, no error (unless you miscount).