Reliably detecting if Mission Control is active

Several topics in the forum ask for a way of detecting whether or not Mission Control is currently active, but the answer has always been that there is no good way of doing so, eg:

I've adapted a StackOverflow answer (written in Obj-C) into this very simple swift script, which has been working reliably enough for me for a couple of months now (tested on both Mojave and Monterey).

I've compiled this into a binary on my own machine* in order to use it within BTT's Execute Shell Script / Task action , or via an Applescript do shell script… in order to query whether Mission Control is currently active at any given time.

My scripting/swift knowledge is pretty basic, so YMMV. But I figured I'd post it in the hope that some may find it useful. As I said, it only returns the status upon invoking it, so some creative thinking and workarounds are required if you want to poll for changes to Mission Control's active status (eg. to close a webview whenever Mission Control is activated). It'd be great if @Andreas_Hegenberg could take a look and see if there is any way to incorporate such polling to maybe populate a BTT variable (which would hopefully reduce the CPU overhead when compared to running it repeatedly via a shell script action). I did have a look at the BetterTouchToolPlugins repo to see if this was something I could implement myself, but unfortunately this is beyond me.



*Requires Xcode commandline tools installed, then run:

swiftc /path/to/missionControlActive.swift -o /usr/local/bin/missionControlActive

(or replace "/usr/local/bin" with wherever you install binaries on your own machine)

ah good point - with the new advanced trigger conditions in BTT I could make a check for mission control available as they are only evaluated when the trigger is triggered.

Yes that'd certainly be useful! Regarding the specific example I mentioned, is there anyway to query those advanced trigger conditions directly from a within a BTT webview?

not yet, but I’ll soon make all the advanced trigger condition variables available as standard BTT variables

That'd be awesome. Now I'm just hoping my naive code stands up to a bit of scrutiny and testing :crossed_fingers:

I already have various mission control checks for special cases in BTT, so unfortunately I won’t need yours - but still thanks a lot for providing it and reminding me about it!

No worries :+1:

By the way, until this is implemented as a separate condition, you can use this advanced trigger condition (I think it does pretty much the same as your swift code)


visible_window_list CONTAINS "Dock - (null)"

That Dock - (null) window is only active when mission control is active as far as I know.

That is certainly simpler than compiling a whole separate binary! Any ETA on when you hope to add the trigger conditions as BTT variables?

In the meantime, I've used your suggestion and figured a workaround by adding 2 triggers with opposing advanced trigger conditions, each setting the same variable ("activeMissionControl") to a different value:

[
  {
    "BTTGestureNotes" : "Mission Control INACTIVE",
    "BTTTriggerConditionsReadOnly" : "NOT (visible_window_list CONTAINS \"Dock - (null)\")",
    "BTTTriggerConditions" : "YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICVRyb290gAGvEBgLDBMYHCAoMjM2PUFGR0pOUlZXWmBkaGpVJG51bGzTDQ4PEBESXxAXTlNDb21wb3VuZFByZWRpY2F0ZVR5cGVfEA9OU1N1YnByZWRpY2F0ZXNWJGNsYXNzEACAAoAX0hQPFRdaTlMub2JqZWN0c6EWgAOAFtMNDg8ZGhIQAoAEgBfSFA8dF6EegAWAFtQPISIjJCUmJ18QEU5TUmlnaHRFeHByZXNzaW9uXxAQTlNMZWZ0RXhwcmVzc2lvbl8QE05TUHJlZGljYXRlT3BlcmF0b3KAFYAQgAaAE9UpKissDy0uLzAxWU5TT3BlcmFuZF5OU1NlbGVjdG9yTmFtZV8QEE5TRXhwcmVzc2lvblR5cGVbTlNBcmd1bWVudHOACIAHEAOACoAPXHZhbHVlRm9yS2V5OtIrDzQ1EAGACdI3ODk6WiRjbGFzc25hbWVYJGNsYXNzZXNfEBBOU1NlbGZFeHByZXNzaW9uozk7PFxOU0V4cHJlc3Npb25YTlNPYmplY3TSFA8+QKE_gAuADtMPK0JDREVZTlNLZXlQYXRogA0QCoAMXxATdmlzaWJsZV93aW5kb3dfbGlzdNI3OEhJXxAcTlNLZXlQYXRoU3BlY2lmaWVyRXhwcmVzc2lvbqNIOzzSNzhLTF5OU011dGFibGVBcnJheaNLTTxXTlNBcnJhedI3OE9QXxATTlNLZXlQYXRoRXhwcmVzc2lvbqRPUTs8XxAUTlNGdW5jdGlvbkV4cHJlc3Npb27TUysPVBBVXxAPTlNDb25zdGFudFZhbHVlgBGAEl1Eb2NrIC0gKG51bGwp0jc4WFlfEBlOU0NvbnN0YW50VmFsdWVFeHByZXNzaW9uo1g7PNQPW1xdXhAQX1pOU01vZGlmaWVyV05TRmxhZ3NeTlNPcGVyYXRvclR5cGWAFBBj0jc4YWJfEBVOU0luUHJlZGljYXRlT3BlcmF0b3KjYWM8XxATTlNQcmVkaWNhdGVPcGVyYXRvctI3OGVmXxAVTlNDb21wYXJpc29uUHJlZGljYXRlo2VnPFtOU1ByZWRpY2F0ZdI3OE1pok080jc4a2xfEBNOU0NvbXBvdW5kUHJlZGljYXRlo2tnPAAIABEAGgAkACkAMgA3AEkATABRAFMAbgB0AHsAlQCnAK4AsACyALQAuQDEAMYAyADKANEA0wDVANcA3ADeAOAA4gDrAP8BEgEoASoBLAEuATABOwFFAVQBZwFzAXUBdwF5AXsBfQGKAY8BkQGTAZgBowGsAb8BwwHQAdkB3gHgAeIB5AHrAfUB9wH5AfsCEQIWAjUCOQI+Ak0CUQJZAl4CdAJ5ApAClwKpAqsCrQK7AsAC3ALgAukC9AL8AwsDDQMPAxQDLAMwA0YDSwNjA2cDcwN4A3sDgAOWAAAAAAAAAgEAAAAAAAAAbQAAAAAAAAAAAAAAAAAAA5o=",
    "BTTTriggerType" : 643,
    "BTTTriggerTypeDescription" : "Named Trigger: btt.testMissionControl",
    "BTTTriggerClass" : "BTTTriggerTypeOtherTriggers",
    "BTTPredefinedActionType" : 281,
    "BTTPredefinedActionName" : "Run Real JavaScript",
    "BTTRealJavaScriptString" : "(async () => {\n    await callBTT(\"set_string_variable\", { variable_name: \"activeMissionControl\", to: \"0\" });\n    returnToBTT(0);\n})();",
    "BTTTriggerName" : "btt.testMissionControl",
    "BTTEnabled2" : 1,
    "BTTAlternateModifierKeys" : 0,
    "BTTRepeatDelay" : 0,
    "BTTUUID" : "2D6090F7-B9A4-4C8B-AB7D-1DB01AE92371",
    "BTTNotesInsteadOfDescription" : 0,
    "BTTEnabled" : 1,
    "BTTModifierMode" : 0,
    "BTTOrder" : 10,
    "BTTDisplayOrder" : 0
  },
  {
    "BTTGestureNotes" : "Mission Control ACTIVE",
    "BTTTriggerConditionsReadOnly" : "visible_window_list CONTAINS \"Dock - (null)\"",
    "BTTTriggerConditions" : "YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICVRyb290gAGvEBYLDBMYICorLTQ4PT5BRUlOT1JYXGBiVSRudWxs0w0ODxAREl8QF05TQ29tcG91bmRQcmVkaWNhdGVUeXBlXxAPTlNTdWJwcmVkaWNhdGVzViRjbGFzcxABgAKAFdIUDxUXWk5TLm9iamVjdHOhFoADgBTUDxkaGxwdHh9fEBFOU1JpZ2h0RXhwcmVzc2lvbl8QEE5TTGVmdEV4cHJlc3Npb25fEBNOU1ByZWRpY2F0ZU9wZXJhdG9ygBOADoAEgBHVISIjJA8lJicoKVlOU09wZXJhbmReTlNTZWxlY3Rvck5hbWVfEBBOU0V4cHJlc3Npb25UeXBlW05TQXJndW1lbnRzgAaABRADgAiADVx2YWx1ZUZvcktleTrSIw8QLIAH0i4vMDFaJGNsYXNzbmFtZVgkY2xhc3Nlc18QEE5TU2VsZkV4cHJlc3Npb26jMDIzXE5TRXhwcmVzc2lvblhOU09iamVjdNIUDzU3oTaACYAM0w8jOTo7PFlOU0tleVBhdGiACxAKgApfEBN2aXNpYmxlX3dpbmRvd19saXN00i4vP0BfEBxOU0tleVBhdGhTcGVjaWZpZXJFeHByZXNzaW9uoz8yM9IuL0JDXk5TTXV0YWJsZUFycmF5o0JEM1dOU0FycmF50i4vRkdfEBNOU0tleVBhdGhFeHByZXNzaW9upEZIMjNfEBROU0Z1bmN0aW9uRXhwcmVzc2lvbtNKIw9LTE1fEA9OU0NvbnN0YW50VmFsdWWADxAAgBBdRG9jayAtIChudWxsKdIuL1BRXxAZTlNDb25zdGFudFZhbHVlRXhwcmVzc2lvbqNQMjPUD1NUVVZMTFdaTlNNb2RpZmllcldOU0ZsYWdzXk5TT3BlcmF0b3JUeXBlgBIQY9IuL1laXxAVTlNJblByZWRpY2F0ZU9wZXJhdG9yo1lbM18QE05TUHJlZGljYXRlT3BlcmF0b3LSLi9dXl8QFU5TQ29tcGFyaXNvblByZWRpY2F0ZaNdXzNbTlNQcmVkaWNhdGXSLi9EYaJEM9IuL2NkXxATTlNDb21wb3VuZFByZWRpY2F0ZaNjXzMACAARABoAJAApADIANwBJAEwAUQBTAGwAcgB5AJMApQCsAK4AsACyALcAwgDEAMYAyADRAOUA+AEOARABEgEUARYBIQErAToBTQFZAVsBXQFfAWEBYwFwAXUBdwF8AYcBkAGjAacBtAG9AcIBxAHGAcgBzwHZAdsB3QHfAfUB+gIZAh0CIgIxAjUCPQJCAlgCXQJ0AnsCjQKPApECkwKhAqYCwgLGAs8C2gLiAvEC8wL1AvoDEgMWAywDMQNJA00DWQNeA2EDZgN8AAAAAAAAAgEAAAAAAAAAZQAAAAAAAAAAAAAAAAAAA4A=",
    "BTTTriggerType" : 643,
    "BTTTriggerTypeDescription" : "Named Trigger: btt.testMissionControl",
    "BTTTriggerClass" : "BTTTriggerTypeOtherTriggers",
    "BTTPredefinedActionType" : 281,
    "BTTPredefinedActionName" : "Run Real JavaScript",
    "BTTRealJavaScriptString" : "(async () => {\n    await callBTT(\"set_string_variable\", { variable_name: \"activeMissionControl\", to: \"1\" });\n    returnToBTT(1);\n})();",
    "BTTTriggerName" : "btt.testMissionControl",
    "BTTEnabled2" : 1,
    "BTTAlternateModifierKeys" : 0,
    "BTTRepeatDelay" : 0,
    "BTTUUID" : "3DA12C71-465D-4FEB-B191-0102584C986C",
    "BTTNotesInsteadOfDescription" : 0,
    "BTTEnabled" : 1,
    "BTTModifierMode" : 0,
    "BTTOrder" : 13,
    "BTTDisplayOrder" : 0
  }
]


From a webview or JS, I can then infer whether Mission Control is active at any time using:

async function activeMissionControl() {
    await callBTT("trigger_named_async_without_response", { trigger_name: "btt.testMissionControl" });
    return !!parseInt(await callBTT("get_string_variable", { variable_name: "activeMissionControl" }));
};

I realise it seems a complete antipattern to await a call to trigger_named_async_without_response, but for some reason it doesn't work with a regular trigger_named.

I also tried returning a result directly from the named trigger without the intermediary variable, but couldn't get this to work at all. Even when using Applescript or a shell script, which I originally thought contradicts this part of the documentation. However I then realised that line of text is repeated elsewhere in that same page but for other actions. @Andreas_Hegenberg please could you clarify that this limitation in fact refers to the runAppleScript() and runShellScript() JS-specific actions, as opposed to a named trigger calling an Apple Script or Shell Script action? The difference in wording is subtle, but I wasted a good couple of hours down that particular rabbit hole :sweat_smile:.

Anyway, I've done a bit of testing with polling this method within a webview, and it does indeed seem a lot less resource-intensive than using a runShellScript call with the compiled binary as I originally suggested :crossed_fingers:.

A named trigger that has an apple script or shell script action assigned should return a value when called like this:

    let result = await callBTT("trigger_named", { trigger_name: "btt.testMissionControl" });

Earlier today I also uploaded an alpha that allows to query the advanced trigger condition variables, so I think you can now just use this:

async function activeMissionControl() {

    return await callBTT("get_number_variable", { variable_name: "missioncontrol_active" });
};

Thanks for the quick response! Just downloaded the alpha and can confirm querying the "missioncontrol_active" number variable does indeed work.

Regarding the issues returning a value directly from a named trigger, I did a quick bit of further testing and realised that it only works when there are no duplicate triggers enabled (regardless of whether they have opposing advanced trigger conditions). However, when only one such trigger exists / is enabled, the value returned is null when its condition is not met. Which negates the need for a duplicate trigger in the first place and is also is potentially useful for other scenarios.

In any case, thanks for looking at this all and providing a much easier solution :+1:.