Triggered Twice, Executed Once? [Solved]

I know there are a few people on this forum who have a lot of software development experience. I would appreciate help in reviewing a possible approach to managing a situation I'm encountering.

I'm working on a system for tracking what the frontmost windows are in each Desktop Workspace. I have two triggers, Window Changed and Space Changed, that are often going to be triggered at the same time (or within microseconds) and sometimes only one or only the other, and there are tasks that I want done by either of them, whenever either is triggered, but not done by both, and I don't know which of the two triggers will be first or if it will be first under all circumstances.

This sounds like a situation that may occur often in multitasking UI systems, so I imagine that there are multiple solutions that have been worked out over the years. But I am not familiar with any of these solutions and don't know which will mostly be fastest, under what conditions, or which will have fewer logistical problems or risks, or which may be more easily implemented in KBM.

I posed the above question to Claud.ai and got the following response:

{ Begin Claude:

You've identified a classic concurrency problem! This is indeed common in UI systems. Let me outline the main solutions and which might work best for KBM:

Common Solutions:

1. Debouncing/Coalescing

  • Both triggers set a "pending update" flag and schedule a single handler to run after a brief delay (e.g., 0.1 seconds)
  • If both triggers fire within that window, only one handler executes
  • Pros: Simple, prevents duplicate work
  • Cons: Adds latency, may feel sluggish

2. Mutex/Lock Pattern

  • First trigger to fire sets a "processing" lock
  • Second trigger sees the lock and exits immediately
  • First trigger releases lock when done
  • Pros: No duplicate work, immediate response
  • Cons: Second trigger's info might be lost

3. State Comparison (Idempotent Operations)

  • Both triggers run the same handler
  • Handler compares current state to last recorded state
  • Only acts if something actually changed
  • Pros: Doesn't matter which fires first or if both fire
  • Cons: Handler runs more often, must check all conditions

4. Event Queue

  • Each trigger adds its event to a queue
  • Single processor handles queue with deduplication logic
  • Pros: No events lost, ordered processing
  • Cons: Complex to implement in KBM

My Recommendation for Your Case:

Option 3 (State Comparison) is probably best because:

  • KBM macros are fast enough that running twice isn't a problem
  • Your operations are naturally idempotent (checking "did this actually change?")
  • No risk of lost information
  • Simpler logic—no timing dependencies

Implementation approach:

Create a single "Update Window State" handler that both triggers call:

1. Get current Space name
2. Get current frontmost window (app, title, ID)
3. Compare to LastRecordedSpace and LastRecordedWindow
4. If Space changed: [Space change logic]
5. If Window changed: [Window change logic]
6. If both changed: [Combined logic]
7. Update LastRecordedSpace and LastRecordedWindow

The key is determining what changed by comparison, not by which trigger fired.

Does this match your intuition? Or do you see specific operations where you absolutely need to know which trigger fired first?

End Claude }

If you've read this far and have any experience with this kind of problem or if you follow the logic and have questions about any part, what do you think? Is the suggestion from Claude resonable?

One thing that the Claude outline does not mention is how to have the shared handler only run once when both triggers happen in close succession. Is this a job for Semaphores? I've never used them. How would that work?

Should I have one macro that is defined to be triggered by either trigger or should I have two separate macros that each simply respond to their own trigger by executing the handler? Would the first be faster? Would the second be more flexible, allowing thing to happen before and after the handler that are specific to that trigger-type and which are independent of what the shared handler is doing?

I'm still very much in the planning phase and I have no experience. And I don't trust to Claude to be more that reasonable-sounding.

Suggestions?

Solved

Solution by @_jims in Comment #5

and by @Nige_S in Comment #6

The actual problem turned out to have a different solution than the orginal question. The above posts provide the solution to the question. The actual KBM design problem was solved in Post #10:

Are there also tasks you do want done by both? Or is enough to say "If this macro is already running then do nothing on the second trigger"?

Thanks for the clarifying question. No, if the Front Window Changed trigger and the Space Changed trigge are both activated, there is nothing that should be done twice, once for each. I think. I'm pretty sure.

Then, assuming that the macro runs long enough that it is still executing when the second trigger fires, it sounds like a "Semaphore Lock" is just the job.

Put it at the start of the macro with its Action Timeout settings as

...and the instance executed by the first firing trigger will lock the semaphore, the instance executed by the second firing trigger will pause because the lock is already taken then timeout and abort.

It's the "Mutex/Lock Pattern" from Claude's suggestion list.

2 Likes

Please help me understand how this works. It looks to me like what I want is a combination of methods 2 and 3. I would have both triggers call the same Handler, with possible trigger-specific actions before and/or after. I would use the Semphore Lock to ensure that the Handler is only run once when called by both the Window Trigger and the Space Trigger in quick succession. Because I want the Handler to only run once, when it runs I can't tell if if was called by one or both triggers, so I have to do the State Comparison.

But I don't want the overhead of determining the current Desktop Workspace at every Window change, so I think I have to think this through some more. Maybe a flow chart would help.

Thanks.

Hi, @August.

I suspect the simple test macros below will help you understand @Nige_S's suggestion (i.e., apply the Semaphore Lock action).


Download: _Common.kmmacros (2.0 KB)

Macro-Image


Download: _🔴 Active Space Changes.kmmacros (2.6 KB)

Macro-Image


Download: _🟢 Focused Window Title Changes.kmmacros (2.7 KB)

Macro-Image


If you want to follow along, open the Keyboard Maestro Engine.log (Help > Open Logs Folder) with the macOS Console app. (Optionally, enable the Log action that is disabled in each of the two calling macros.)


Based on my testing, if the Desktop Space is changed, :green_circle: triggers first, otherwise :red_circle: triggers (assuming the focused window title changes).

_Common, of course, could include some actions that run regardless of the initiating macro and then others that depend on the %TriggerValue%.

I think that what you need to do is draw a flow diagram -- this has gone well beyond the subject line's simple "execute once, however many instances get triggered".

A flow diagram will help firm up the logic, show the various context-dependent execution paths, reveal what states need to be tested/saved to variables. And, importantly, is a lot easier to understand than paragraphs of text!

And I've just re-read your final paragraph -- great minds think alike!

That's what an "insta-quit" Semaphore does. If you only want it to run once, why does it matter if there was an attempt to run it a second time? The answer to that may determine the best way to go about things.

A Semaphore lock is generally used to form a "first in, first out" (FIFO) queue for a particular "resource". Run this demo by clicking the Editor's Run button 3 or 4 times, then deliberately press and release (not quick-tap) the ⌘ key. With the Lock Action disabled all instances execute together, as you can see from the MILLISECONDS() value.

Now enable the Lock Action and repeat the test -- this time you'll get one instance running to completion for every press-and-release of the ⌘ key. We've formed a FIFO queue for the "has the ⌘ key been pressed and released" resource.

FIFO demo.kmmacros (4.1 KB)

Image

If you then change that first Action's timeout settings to "1/100th seconds, abort, don't Notify" you'll have the commonly-used "multiple trigger events, only one execution" -- probably the most common use of Semaphores in KM:

FIFO Insta-Quit demo.kmmacros (4.2 KB)

Image

There is no "Semaphore name is locked" Condition (though, IIRC, @Airy made a feature request which got a positive response from @peternlewis ). But that doesn't mean we can't make our own! A "Semaphore" Action can be used to abort a macro, a "Try/Catch" Action will let us Catch the abort and do something else instead. Click Run once and you'll see KM spinning its menu bar icon, waiting for the ⌘ keypress. Click Run another couple of times and those instances will execute to completion immediately. Press the ⌘ key and the first-triggered instance will now complete.

Bypass Queue Demo.kmmacros (6.6 KB)

Image

And if you'd like something more "If... Then... Else", use the same trick to set a variable then test that and branch accordingly:

Semaphore If-Then-Else Demo.kmmacros (8.4 KB)

Image

And, obviously, this can all be put together as you need -- a separate macro for each trigger, using the same Semaphore, with their "shared code" hived out into a submacro whose execution is in an "If" conditional on Semaphore state, for example.

Hopefully the above will help clear things in your mind -- but if all I've done is muddy the waters further, my apologies! Ask away if anything doesn't make sense.

1 Like

It has, thank you very much. I want to really understand the test/sample/demo macros and make that multi-state flow chart (I think there's four states to trace). However, it's late and I may not get to it until this weekend.

I agree that the initial question has been reasonably answered, but it's just a method; the larger question of "what is my actual problem that I think needs this method as part of its solution" is what we've drifted into. Do you think that's worth a separate thread or rephrasing the title?

1 Like

IMO, start afresh.

But I'm betting you won't need to. Now you've some ideas of how you can use Semaphores as part of your flow control, when you nail down your various process flows you'll find everything falls into place.

I have faith in you, even if you don't :wink:

2 Likes

Thanks Nige @Nige_S and Jim @_jims,

Thank you so much for your explanations, sample macros (which very much helped and gave me ideas), and especially for helping me clarify how I was thinking about the whole problem.

It turns out that I've been overthinking and overcomplicating the whole thing (who, me?!).

TL;dr: I don't need the method in this thread, it's way too general and my problem is specific enough that contantly monitoring window changes is overkill.

The details:

I had felt stymied for a long time that there was no way to get a simple list of all the windows in a Desktop Workspace so that I could find out what the second window was, when I needed to. ("When I needed to" seemed simple enough: when I activated a DSID window (Desktop Space ID) to produce a Space Change, the DSID window would always, necessarily, be frontmost, and I wanted to bring the previous frontmost, now second window to the front.)

But tokens like %Window%2% or %Application%2% don't work, they are not pointing to what I need. Over two years ago I asked that question, how to determine the second window, in the thread "Is it Possible to Identify the Second Window in a Desktop/Space When it is in a Different Application from the frontmost App?". About a week later, @CJK came up with a JXA script, but unfortunately he caveated it with:

And I never heard back. CJK's thought that it gave all windows, not just the current desktop was discouraging. Lots of research has told me that Apple no longer makes "which Desktop Space" public, as of (IIRC) OSX 10.10, and that would be necessary. So I ignored his JXA script. (Which turns out to have been foolish.)

The problem has been percolating in my mind for two years. It's been an annoyance, but I've been working around it and it only actually trips me up every few days. Every time it does, I think about fixing it and whittling away at my notes on the problem. My recent thought was that if Apple won't let me just query for the list of windows in the current Desktop, then I'd just have to build my own. After a couple of false starts, I was lately working on the idea of maintaining each Desktop's list of windows by continuously tracking each Window Title Change in a multidimensional array with one row for each Desktop.

While hacking away at the problem in this thread, here, I ran across that older thread with CJK's JXA script, so I decided to try the script myself (finally!). I couldn't get it to run (I believe now it was due to constructions like Ω = and let Wᵢ = that didn't translate, I think, in KBM's "Run Javascript" action) so I revived the thread and asked for help. Thankfully, @_jims created a sample macro that used CJK's JXA script and which ran for me.

Lo! And Behold! It only lists the current Desktop! Hallelujah!

(At first it doesn't give me the window title (just "undefined") yet it works for Jim, but that's just details.)

I also realized that, as I said, I've been overthinking and overcomplicating the issue. I don't have to be on top of every possible change nor do I need to track things with the Space Changed trigger.

There are three ways that I may change to a different Space:

  • When I use a menu or hotkey to activate the DSID (Desktop Space ID) file on the target Desktop.
  • When I use a menu or hotkey to drag a window from the current Desktop Space to the target Desktop.
  • When an app changes the focus to a window in a different Space.
    • E.g., I click a link in a PDF and there is no open browser window in the current Desktop so the URL gets opened in an existing browser window in another Desktop.
    • E.g., I'm in Finder and I use the Window menu to switch to an existing Finder window that happens to be in another Desktop.

In the 2nd and 3rd cases, I do not need to know about previously focussed windows in the target Desktop because the method of switching Desktops always determines the window that should be frontmost. And that is not my DSID window. No problem.

It's only in the first case that I want to activate whatever window had been frontmost when I previously left that Desktop. The menu or hotkey I used to change Desktops has activated the DSID window, so I want to know what is the second window in that Space and activate it.

When I leave a Desktop Space via either of the first two cases, I have the opportunity to capture the frontmost window details to be able to restore it later. But when I leave a Desktop Space via the 3rd case, there is no way to intercept the process before the change happens. So in that case I don't have an accurate record of what to restore whenever I may return to that Desktop Space.

Most of the time I don't need the list of windows, I just need to save the details of the frontmost window when I use a menu or hotkey to go to another Space. When I get to the new Space, I need to determine if saved window details exist and if they do, then activate that window. If they don't exist, only then do I need to rn CJK's JXA script to find out what the second window is (the first is the DSID window) and then activate it. I won't be running the JXA script at every Space change, I will only need it when I return to a Desktop that I had left via an app window change and not via a menu or hotkey choice. And that's relatively rare (especially compared to continuously tracking every window change).

So, thanks Nige and Jim, for all your help in understanding Semaphores and in getting the logic of what I really needed straight in my head.

2 Likes

If you've been following me this far, you might be interested in this.

I took the description above, starting at "There are three ways that I may change to a different Space", and gave it Claude.ai and asked for a diagram. It gave me a diagram and a Mermaid file. The diagram, as displayed within Claude.ai, had some formatting glitches, e.g., yellow text on a white box, so I just pasted the Mermaid text into draw.io.

Here's what I got: