18

I have long used an Applescript triggered from a keyboard shortcut to dismiss notifications on MacOS. It worked by simulating a click of the "close" button of every open Notification Center window.

Since upgrading to Big Sur (currently 11.0.1), notifications no longer have a close button. Instead, when you hover over them with the mouse, an "X" button will appear. For a person who has difficulty clicking precisely (I have a slight hand tremor), using the mouse/trackpad is difficult.

Anyone have suggestions on how to restore that functionality so I can just keep my hands on the keyboard?

My Settings

I am using Automator to test this.

In System Preferences > Notifications, I selected Automator:

  • Allow Notifications is true.
  • Automator alert style: is Alerts.
  • Notification grouping is automatic.

Testing

  • I opened Automator and created a new workflow.
  • I added three instances of the Display Notification action.
  • I gave them Title: values of test 1, test 2, and test 3.
  • And Subtitle: values of subtitle 1, subtitle 2, and subtitle 3.
  • And Message: values of message 1, message 2, and message 3.
  • I ran the workflow using ⌘+R.
  • In the upper-right corner of my screen, I see a notification from Automator stacked over two others. Only notification test 3 is fully visible and it has the notation "2 more notifications" at the bottom.

@AndrewJanian 's Script

  • Again, thank you, Andrew for providing the script!
  • I added the line #!/usr/bin/osascript to the start of the script and saved it as andrew.applescript.
  • I made the file executable. (chmod 755 andrew.applescript).
  • I ran it in a terminal window (./andrew.applescript)
  • The result of running is:
    • the grouped notifications become ungrouped (so I see test 3, test 2, and test 1 stacked top to bottom in the notification area
    • the terminal displays the following error
      ./andrew.applescript:370:377: execution error: System Events got an error: Can’t get action "Name:Clear All
      Target:0x60000116ff20
      Selector:_internal_handleCustomActionWithUiAction:" of group 1 of UI element 1 of scroll area 1 of window "Notification Center" of application process "NotificationCenter". (-1728)
      

I'll try messing around with it to see if I can make it work with modifications.

Note: If I run the script again, starting from the ungrouped state:

  • it dismisses the top-most notification (test 3)
  • and it displays the following in the terminal:
    action Name:Close
    Target:0x60000116ff20
    Selector:_internal_handleCustomActionWithUiAction: of group 1 of UI element 1 of scroll area 1 of window Notification Center of application process NotificationCenter
    
5
  • 1
    If notification is set to Banner type then it will be dismissed automatically after a while, no interaction is needed. – Aivar Paalberg Dec 4 '20 at 14:34
  • 1
    Thanks, @AivarPaalberg. I really want the notifications to stick around until I dismiss them. I just want to be able to dismiss them without using the mouse. [Instead of the old 'Close' button, now there is a tiny X to click on—it's almost like they wanted to make it as small as the "close" button for mobile ads. :-) – Colin Fraizer Dec 9 '20 at 7:10
  • You can dismiss notifications without aiming at tiny X if you happen to have trackpad. While hovering anywhere over notification two finger swipe from left to right should dismiss it. From productivity perspective I personally don't believe that on desktop notifications and immediate interaction with them is good workflow. – Aivar Paalberg Dec 9 '20 at 10:22
  • 1
    Thank you, @AivarPaalberg. That certainly helps, but I agree it's less than ideal to have to move my mouse cursor up to that corner just to dismiss the notifications. I prefer my old workflow of running a command (okay, pressing a key combo) to make them go away while keeping my focus on whatever was interrupted. – Colin Fraizer Dec 9 '20 at 19:57
  • And the cross is really very tiny. So tiny that I just noticed it... There is a UI issue here, Apple. – Snicolas Mar 18 at 19:57
9

Update

I'm posting a most robust version that evolved from my initial post. This seems to do a reasonable job of closing all windows, but it's sometimes slow to execute. As I've stated in the comments, I don't know much about AppleScript, so perhaps someone who knows what they're doing can tell us how to improve the performance.

activate application "NotificationCenter"
tell application "System Events"
    tell process "Notification Center"
        repeat
            
            try
                set theWindow to group 1 of UI element 1 of scroll area 1 of window "Notification Center"
            on error
                exit repeat
            end try
            
            try
                set theActions to actions of theWindow
                
                # Try to close the whole group first. If that fails, close individual windows.
                repeat with theAction in theActions
                    if description of theAction is "Clear All" then
                        set closed to true
                        tell theWindow
                            perform theAction
                        end tell
                        exit repeat
                    end if
                end repeat
                
                repeat with theAction in theActions
                    if description of theAction is "Close" then
                        set closed to true
                        tell theWindow
                            perform theAction
                        end tell
                        exit repeat
                    end if
                end repeat
                
            end try
        end repeat
    end tell
end tell

Original

Yes, thank you for posting the script, @AndrewJanian.

I received an error similar to @ColinFraizer's when using it as is. I think the issue is the inner repeat over the actions: it's trying to access actions in some cases after the window is closed. A minor tweak fixes this issue for me:

activate application "NotificationCenter"
tell application "System Events"
    tell process "Notification Center"
        set theWindow to group 1 of UI element 1 of scroll area 1 of window "Notification Center"
        # click theWindow
        set theActions to actions of theWindow
        repeat with theAction in theActions
            if description of theAction is "Close" then
                tell theWindow
                    perform theAction
                end tell
                exit repeat
            end if
        end repeat
    end tell
end tell
4
  • Does it work on big sur ? I'm getting System Events got an error: Can’t get process "Notification Center" here. – Amadeu Cavalcante Filho Mar 6 at 17:26
  • When I run this script in the Script Editor on macOS Big Sur 11.2 it dismisses the topmost notification only, but not all notifications. – Employee Mar 10 at 0:00
  • @AmadeuCavalcanteFilho Yes, I run it on Big Sur. I'm a total AppleScript newbie, so I'm afraid I don't know what's causing that particular error. – lunzwell Mar 12 at 4:13
  • @Employee Yes! I will update the post to add a more recent version that I have wrangled into a more robust form. The updated version does a pretty reasonable job of clearing all notifications for me, although it seems slow. I don't know the first thing about writing good AppleScript, so perhaps someone who knows what they're doing can improve on this. – lunzwell Mar 12 at 4:15
5

I have an AppleScript that works. I found the elements using UIBrowser. Caveat is that the notification must have a "Close" action. All of the notifications I have encountered have that action.

activate application "NotificationCenter"
tell application "System Events"
    tell process "Notification Center"
        set theWindow to group 1 of UI element 1 of scroll area 1 of window "Notification Center"
        click theWindow
        set theActions to actions of theWindow
        repeat with theAction in theActions
            if description of theAction is "Close" then
                tell theWindow
                    perform theAction
                end tell
            end if
        end repeat
    end tell
end tell
1
  • 2
    Thanks, @Andrew. Sadly, this doesn't work for me on Big Sur. I'd love to explain more, but the comment is character limited and won't let me add line breaks, making it very annoying. I'm going to edit my question to give the context. – Colin Fraizer Dec 9 '20 at 7:07
2

This has been annoying me too, but I managed to get something close working today.

First I installed cliclick, a command line tool that lets you simulate mouse movement and clicks. It's available via Homebrew with brew install cliclick.

Then I figured out the coordinates of the time in the top-right, which when clicked, opens the notification centre. This took a bit of experimentation.

Then I figured out the coordinates of the close button that appears on hover. Again this took a bit of experimentation.

Then I wrote a script that uses cliclick and these coordinates to open the Notification Center, move the mouse over where the "clear" button appears, sleeps for 500 milliseconds to give it time to appear, and then click it. It worked!

export PATH=/usr/local/bin:$PATH # make sure cliclick binary is on the path
cliclick c:1900,10               # click on the time to open Notification Center
cliclick -w 500 m:1570,45 c:.    # move mouse over the close button, wait for it to appear, then click it

I then tried to get this running as an Automator service. I got it running from within Automator but though the script worked there, it refused to work when I assigned it to a keyboard shortcut. I bashed my head against a brick wall trying out different permissions etc until giving up.

I tried the HotKey app, but had the same result.

Eventually I thought about trying an Alfred workflow, so I could open Alfred with Cmd+Space and then type "Clear notifications" to clear them. Much more verbose than a keyboard shortcut but... it worked! And no mouse needed. My best guess as to why is that when Alfred runs the script, it always runs from within Alfred, and Alfred already has the required permissions to move the mouse and click on stuff, but when I try to attach an Automator service to a keyboard shortcut, that service runs with whatever permissions the currently in focus app has.

This feels like a horribly brittle solution (what happens when I'm not using my external monitor??) and it seems rubbish that there's no built-in way to do this, but at least it works.

TL;DR a combination of a script using cliclick and an Alfred workflow to run it.

2

Thank you @lunzwell. I added a error routine.

on run
    try
        activate application "NotificationCenter"
        tell application "System Events"
            tell process "Mitteilungszentrale"
                set theWindow to group 1 of UI element 1 of scroll area 1 of window "Notification Center"
                # click theWindow
                set theActions to actions of theWindow
                repeat with theAction in theActions
                    if description of theAction is "Schließen" then
                        tell theWindow
                            perform theAction
                        end tell
                        exit repeat
                    end if
                end repeat
            end tell
        end tell
    on error e
        display dialog e
        activate
    end try
end run

I wonder though if there is a way to avoid an error message, when accidentally running the script with no notifications on the screen.

2
  • You also translated the script to German. :D – mattsson Jan 6 at 14:07
  • Yep, sorry. I forgot about that. I needed to do that in order to work on my machine. I would be great if that would not be necessary. – Ptujec Jan 7 at 19:25
2

I found an Automator JavaScript action which works:

function run(input, parameters) {

  const notNull = (val) => {
    return val !== null && val !== undefined;
  }

  const appName = "";
  const verbose = true;

  const CLEAR_ALL_ACTION = "Clear All";
  const CLOSE_ACTION = "Close";

  const hasAppName = notNull(appName) && appName !== "";
  const appNameForLog = hasAppName ? (" " + appName) : "";

  const log = (message, ...optionalParams) => {
    console.log("[close_notifications] " + message, optionalParams);
  }

  const logVerbose = (message) => {
    if (verbose) {
      log(message);
    }
  }

  const findCloseAction = (group, closedCount) => {
    try {
      let clearAllAction;
      let closeAction;
      for (let action of group.actions()) {
        if (action.description() === CLEAR_ALL_ACTION) {
          clearAllAction = action;
          break;
        } else if (action.description() === CLOSE_ACTION) {
          closeAction = action;
        }
      }
      if (notNull(clearAllAction)) {
        return clearAllAction;
      } else if (notNull(closeAction)) {
        return closeAction;
      }
    } catch (err) {
      logVerbose(`${closedCount}: Caught error while searching for close action, window is probably closed.`);
      logVerbose(err);
      return null;
    }
    log("No close action found for notification");
    return null;
  }

  const notificationGroupMatches = (group) => {
    if (!hasAppName) {
      return true;
    }

    logVerbose(`Checking UI elements of group...`);
    try {
      for (let elem of group.uiElements()) {
        if (hasAppName && elem.role() === "AXStaticText" && elem.value().toLowerCase() === appName.toLowerCase()) {
          return true;
        }
      }
    } catch (err) {
      logVerbose(`Caught error while checking window, window is probably closed.`);
      logVerbose(err);
    }
    return false;
  }

  const closeNextGroup = (groups, closedCount) => {
    for (let group of groups) {
      if (notificationGroupMatches(group)) {
        logVerbose(`${closedCount}: FIND_CLOSE_ACTION`);
        let closeAction = findCloseAction(group, closedCount);

        if (notNull(closeAction)) {
          logVerbose(`${closedCount}: CLOSING`);
          try {
            closeAction.perform();
            logVerbose(`${closedCount}: CLOSE_PERFORMED`);
            return [true, 1];
          } catch (err) {
            logVerbose(`${closedCount}: Caught error while performing close action, window is probably closed.`);
            logVerbose(err);
          }
        }
        return [true, 0];
      }
    }
    return false;
  }

  const getNotificationCenter = () => {
    let systemEvents = Application("System Events");
    return systemEvents.processes.byName("NotificationCenter");
  }

  const getNotificationCenterGroups = () => {
    return getNotificationCenter().windows[0].uiElements[0].uiElements[0].uiElements();
  }

  let notificationCenter = getNotificationCenter();
  if (notificationCenter.windows.length <= 0) {
    return input;
  }

  let groupsCount = getNotificationCenterGroups().filter(group => notificationGroupMatches(group)).length;

  if (groupsCount > 0) {
    logVerbose(`Closing ${groupsCount}${appNameForLog} notification group${(groupsCount > 1 ? "s" : "")}`);

    let closedCount = 0;
    let maybeMore = true;
    while (maybeMore) {
      let closeResult = closeNextGroup(getNotificationCenterGroups(), closedCount);
      maybeMore = closeResult[0];
      if (maybeMore) {
        closedCount = closedCount + closeResult[1];
      }
    }
  } else {
    throw Error(`No${appNameForLog} notifications found...`);
  }

  return input;
}
2

I got a working script via cliclick also. Here's my version of the script I use. I've stitched together from a bunch of user libraries I used. I have it mapped to voice control created via Automator instead of a keyboard shortcut.

It still fails when the notification refuses to show the close button no matter how much you move the pointer on top of it.

activate application "NotificationCenter"
delay 0.1
tell application "System Events" to tell process "Notification Center"
    if (count of windows) is 0 then return 
    
    set theGroup to first group of UI element 1 of scroll area 1 of window "Notification Center"        
    tell theGroup
        if my isStacked(theGroup) then
            click theGroup
            delay 1
        end if
        
        my dismissNotification(theGroup)
    end tell
end tell

on isStacked(nextGroup)
    tell application "System Events" to tell process "Notification Center"          
        get help of nextGroup is "Activate to expand"
    end tell
end isStacked

to dismissNotification(theNotification)
    script CloseButtonClicker
        -- pointer's saveCurrentPosition()
        movePointer at theNotification
        delay 0.1
        tell application "System Events"
            try
                click button "Close" of theNotification
                return true
            end try
            delay 0.1
        end tell
        -- pointer's restorePosition()
    end script  
    exec on CloseButtonClicker for 3 by 0.4  -- Retry on failure up to 3x. Optional.
end dismissNotification

to movePointer at theUi
    set coord to getCoord at theUi
    set formattedCoord to formatCoordinates(item 1 of coord, item 2 of coord)

    set clickCommand to "/usr/local/bin/cliclick -e 1 m:" & formattedCoord
    do shell script clickCommand
end movePointer

to formatCoordinates(x, y)
    if x is less than 0 then set x to "=" & x
    if y is less than 0 then set y to "=" & y

    return x & "," & y as text
end formatCoordinates

to exec on scriptObj by sleep : 1 for ntimes : 1000
    repeat ntimes times
        try
            set handlerResult to run of scriptObj
            if handlerResult is not missing value then return         handlerResult
        end try 
        delay sleep
    end repeat
    return missing value
end exec
1

I made some improvements to this script:

  1. I added error handling (a narrower trap, in English) /ht @Ptujec
  2. I recursively call this function after a delay (because I found that for some notifications, I would close, they would all disappear, but then all the others that were not the top in the stack would re-appear a few seconds later)
  3. I added support for the "Clear All" button (I found sometimes I had to use this instead of "Close")

My full script is in my dotfiles on GitHub here and I have it exposed via an Alfred Workflow here.

#!/usr/bin/osascript
# via:
# https://apple.stackexchange.com/questions/408019/dismiss-macos-big-sur-notifications-with-keyboard

# define a function we can call recursively
on dismiss_notification_center(n)
    log "dismiss_notification_center: " & n
    set performedAction to false
    activate application "NotificationCenter"
    tell application "System Events"
        tell process "Notification Center"
            try
                # when there are no notifications, this may result in:
                # 'System Events got an error: Can’t get window "Notification Center" of process "Notification Center".'
                # This is our recursion base case.
                set theWindow to group 1 of UI element 1 of scroll area 1 of window "Notification Center"
            on error e
                # log the error to the console:
                log quoted form of e
                return
            end try
            # log theWindow

            set theActions to actions of theWindow
            repeat with theAction in theActions
                # log theAction
                # log description of theAction
                if description of theAction is in {"Close", "Clear All"} then
                    tell theWindow
                        perform theAction
                        set performedAction to true
                    end tell
                    exit repeat
                end if
            end repeat
        end tell
    end tell
    # log "performedAction: " & performedAction
    if performedAction
        # for some reason, the loop doesn't close them all when grouped, so
        # we need to recurse. But, first we have to sleep to allow notif to re-appear:
        do shell script "/bin/sleep 3" # min sleep time that worked for me, and sometimes reminders take even longer
        dismiss_notification_center(n + 1)
    end if
end dismiss_notification_center

# first call to recursive function:
dismiss_notification_center(0)

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .