Expose existance & status of menu item in a condition or API call

I'd like to implement an action that moves emails to the mailbox that Apple Mail suggests (which is usually correct) but presents a floating menu if Apple Mail does not suggest anything. The lack of a suggestion can be determined by looking at the menu (there is a deactivated menu item called "Move to predicted mailbox" under "Message"). Similar patterns (menu items changing names based on the app's status) occur in, e.g., Zoom.

It would be great if there was a way to determine if a menu item exists and if it is activated. That functionality must exist internally (to activate menu items). It could be exposed as an Advanced Condition or a Javascript API function (in which case it could be combined with the "If Java Script Returns True" Condition Action).

Thanks a lot.

I can expose this in Apple Script / Java Script, so you could use it with the "If Java Script Returns True" condition. I'l add it with one of the next versions.

Cool, thanks.

Any updates on this? I have been following the release notes but did not see anything. It may have not been included in the notes.

unfortunately not yet but should be ready soon. I'll post here once it's there

Great, thanks. Holding my breath :face_with_spiral_eyes:.

I have added it in 4.458, however I have not tested it much yet.

tell application "BetterTouchTool"
	get_menu_item_details "Edit;Redo"
end tell
async function someJavaScriptFunction() {
	return await get_menu_item_details("View;Show Live View");
}

=>
{"available": true, "enabled": true, "checked": false}

Looks like it is working -- thanks a lot. Is accessing menu items by pattern or position supposed to work (it does not seem like they do).

that should work, it is using the same logic as the „trigger menubar menu item“ action . I’ll have a look tomorrow!

Hi @Andreas_Hegenberg,

Thanks for implementing this. Functionally, it does exactly what it needs to. Unfortunately, it often takes a really long time (more than ten seconds) to return a result. I have an action that checks if Apple Mail suggests/predicts a folder to move a mail to and, if not, presents a menu to select one. Here is the javascript I use to check if Apple Mail predicts a folder:

async function someJavaScriptFunction() {
try{
	let status = await get_menu_item_details("Message;Move to Predicted Mailbox");	
	let parsed = JSON.parse(status)
return !parsed.available
} catch {
	return false
}
}

As this is meant to run interactively, waiting for half a minute for each of these is a no-go. Any ideas?

thanks so much,

Holger

more than 10 seconds is weird. In case something gets blocked inside of BTT the timeout would be 5 seconds.

It could also be Mail taking a long time to respond, but that would also be weird.

Could you maybe post the full example? (or send to andreas@folivora.ai) Then I could try it here.

Just sent you a mail.

thanks,

Holger

I was looking for this function, and I found it. It works, but... somewhat.

I have this menu:

I want to use zoom in and zoom out keyboard shortcuts. So I created simple if-then-else-chains:

You can see the code for the first if-Javascript-returns-true. The other ones are identical, but of course with 2x and 4x instead of 1x. The other shortcut is also the same, but in reverse.

But zoom in only goes to 4x and zoom out stops at 2x. It never goes to 1x or 8x. Am I misunderstanding the logic of if-then-else in BTT?

Note: I tested with ⌘1, ⌘2, ⌘4 and ⌘8 first, but changed to triggering menu items to make sure I got the menu path right. And I did - the symptoms are the same, so the menu path is correct.

Shouldn't the condition in your screenshot return parsed.checked without negating it? (You want to trigger the 2x magnification if 1x is checked, right)

In general, for this I would recommend to use a pure JS approach like this (via the run real java script action)

async function someFunction() {

    let triggerItem = async (menubarPath) => {
        await trigger_action({
            json: JSON.stringify({
                "BTTPredefinedActionType": 124,
                "BTTMenubarPath": menubarPath,
            }), wait_for_reply: false
        });
    }

    let isChecked = async (path) => {
        let status = await get_menu_item_details(path);
        let parsed = JSON.parse(status);
        return parsed.checked === true;
    }



    if (isChecked("View,Magnification;1x")) {
        triggerItem("View;Magnification;2x");
        return "triggered 2x";
    } else if (isChecked("View,Magnification;2x")) {
        triggerItem("View;Magnification;4x");
        return "triggered 4x";
    } else if (isChecked("View,Magnification;4x")) {
        triggerItem("View;Magnification;8x");
        return "triggered 8x";
    } else {
        triggerItem("View;Magnification;1x");
        return "triggered 1x";
    }

}

Yes, of course it should. And in my memory it did, but my screenshot says my memory is lying.

Anyway, I went the same path as you, pure javascript. And for me it always triggered 2x, no matter what it was to begin with. So I tried your script (which is prettier than mine). It is still always triggering 2x. Which means 1x is always checked, even if it doesn't look like it in the menu?

Video:

Maybe try to return the output of the returned status, to see what exactly it recognizes:

async function someFunction() {

    let triggerItem = async (menubarPath) => {
        await trigger_action({
            json: JSON.stringify({
                "BTTPredefinedActionType": 124,
                "BTTMenubarPath": menubarPath,
            }), wait_for_reply: false
        });
    }

    let isChecked = async (path) => {
        let status = await get_menu_item_details(path);
        let parsed = JSON.parse(status);
        return parsed.checked === true;
    }

 let statusString = async (path) => {
        return  await get_menu_item_details(path);
    }



    if (isChecked("View,Magnification;1x")) {
        triggerItem("View;Magnification;2x");
       return await statusString("View,Magnification;1x");
    } else if (isChecked("View,Magnification;2x")) {
        triggerItem("View;Magnification;4x");
        return await statusString("View,Magnification;2x");
    } else if (isChecked("View,Magnification;4x")) {
        triggerItem("View;Magnification;8x");
       return await statusString("View,Magnification;4x");
    } else {
        triggerItem("View;Magnification;1x");
        return await statusString("View,Magnification;8x");
    }

}

It returns false for all values. Enabled, available and checked are false for all four menu items.

arg sorry, it was a typo on my side.

It should be

View;

Not
View,

(comma vs semicolon)

async function someFunction() {

    let triggerItem = async (menubarPath) => {
        await trigger_action({
            json: JSON.stringify({
                "BTTPredefinedActionType": 124,
                "BTTMenubarPath": menubarPath,
            }), wait_for_reply: false
        });
    }

    let isChecked = async (path) => {
        let status = await get_menu_item_details(path);
        let parsed = JSON.parse(status);
        return parsed.checked === true;
    }



    if (isChecked("View;Magnification;1x")) {
        triggerItem("View;Magnification;2x");
        return "triggered 2x";
    } else if (isChecked("View;Magnification;2x")) {
        triggerItem("View;Magnification;4x");
        return "triggered 4x";
    } else if (isChecked("View;Magnification;4x")) {
        triggerItem("View;Magnification;8x");
        return "triggered 8x";
    } else {
        triggerItem("View;Magnification;1x");
        return "triggered 1x";
    }

}

I know, I saw it and now it returns what is expected. But:

{"available": true, "enabled": true, "checked": true}{"available": true, "enabled": true, "checked": false}{"available": true, "enabled": true, "checked": false}{"available": true, "enabled": true, "checked": false}: triggered 2x

{"available": true, "enabled": true, "checked": false}{"available": true, "enabled": true, "checked": true}{"available": true, "enabled": true, "checked": false}{"available": true, "enabled": true, "checked": false}: triggered 2x

{"available": true, "enabled": true, "checked": false}{"available": true, "enabled": true, "checked": false}{"available": true, "enabled": true, "checked": true}{"available": true, "enabled": true, "checked": false}: triggered 2x

{"available": true, "enabled": true, "checked": false}{"available": true, "enabled": true, "checked": false}{"available": true, "enabled": true, "checked": false}{"available": true, "enabled": true, "checked": true}: triggered 2x

Why is is triggering 2x when 1x.checked is false?

Code:

async function someJavaScriptFunction() {
    let triggerItem = async (menubarPath) => {
        await trigger_action({
            json: JSON.stringify({
                "BTTPredefinedActionType": 124,
                "BTTMenubarPath": menubarPath,
            }), wait_for_reply: false
        });
    }

    let isChecked = async (path) => {
        let status = await get_menu_item_details(path);
        let parsed = JSON.parse(status);
        return parsed.checked === true;
    }

	let statusString = async (path) => {
        return await get_menu_item_details(path);
    }

	var statusStrings = await statusString("View;Magnification;1x");
	statusStrings += await statusString("View;Magnification;2x");
	statusStrings += await statusString("View;Magnification;4x");
	statusStrings += await statusString("View;Magnification;8x");

    if (isChecked("View;Magnification;1x")) {
        triggerItem("View;Magnification;2x");
	statusStrings += ": triggered 2x";
    } else if (isChecked("View;Magnification;2x")) {
        triggerItem("View;Magnification;4x");
	statusStrings += ": triggered 4x";
    } else {
        triggerItem("View;Magnification;8x");
	statusStrings += ": triggered 8x";
    }

	alert(statusStrings);
	return statusStrings;

	}

Should have tested my code first :smiley:

the isChecked is an async function, thus needs to be awaited (otherwise it will test the if with the returned promise, which is not undefined and thus evaluates to true)

Still not tested but this was definitely an error:

async function someJavaScriptFunction() {
  const triggerItem = async (menubarPath) => {
    await trigger_action({
      json: JSON.stringify({
        BTTPredefinedActionType: 124,
        BTTMenubarPath: menubarPath,
      }),
      wait_for_reply: false,
    });
  };

  const isChecked = async (path) => {
    const status = await get_menu_item_details(path);
    const parsed = JSON.parse(status);
    return parsed.checked === true;
  };

  const statusString = (path) => get_menu_item_details(path);

  let statusStrings = "";
  statusStrings += await statusString("View;Magnification;1x");
  statusStrings += await statusString("View;Magnification;2x");
  statusStrings += await statusString("View;Magnification;4x");
  statusStrings += await statusString("View;Magnification;8x");

  if (await isChecked("View;Magnification;1x")) {
    await triggerItem("View;Magnification;2x");
    statusStrings += ": triggered 2x";
  } else if (await isChecked("View;Magnification;2x")) {
    await triggerItem("View;Magnification;4x");
    statusStrings += ": triggered 4x";
  } else {
    await triggerItem("View;Magnification;8x");
    statusStrings += ": triggered 8x";
  }

  alert(statusStrings);
  return statusStrings;
}