macOS Big Sur以降で通知をキーボードショートカットで消去する方法

macOS Big Sur以降で通知(通知パネル)をキーボードショートカットで消去(クリア)する方法を紹介。

なお、本記事の方法は通常時のmacOS画面上に表示された通知を消去するものであって、通知センターに溜まった通知の消去まではできない。

Automatorを使用して通知をキーボードショートカットで消去する方法

Automator(macOS付属アプリケーション)を使用する方法は柔軟性があるがアクセシビリティへのアクセス許可が面倒な点に注意。

アクセス許可が面倒な場合は手間はかかるが「スクリプトエディタで単一で実行可能なアプリケーションを作って通知を消去する方法」までスキップ(管理人はこちらの方法をお勧め)。

1. Automatorをアクセシビリティに追加し制御を許可する

まず事前にシステム環境設定の「セキュリティとプライバシー」の「プライバシー」の項目で「アクセシビリティ(①)」を開き、「+ボタン(②)」でAutomatorアプリケーションを追加し制御を許可しておく(③)。

2. クイックアクションを選択

続いてAutomatorを起動して新規書類を作り「クイックアクション」をクリックして右下の「選択」をクリック。

3. JavaScriptを貼り付ける

続いて「JavaScriptを実行(①)」のアクションを右のエリアにドラッグして追加する(②)。

次に「JavaScriptを実行」のアクション内に元からあるコードを削除して下記のJavaScriptをコピペ(下記ボックスはスクロール可)。

function run(input, parameters) {

    const notNull = (val) => {
      return val !== null && val !== undefined;
    }
  
    const appName = "";
    const verbose = true;
  
    const CLEAR_ALL_ACTION = "すべて消去";
    const CLOSE_ACTION = "閉じる";
  
    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;
  }

このスクリプトは有志によって開発されGitHubで公開されているものを管理人が日本語環境用に直しただけなので、この場を借りて改めてスクリプト制作者の方に感謝を申し上げる。

参考 close_notifications_applescript.jsGitHub

4. スクリプトを実行して通知が消えるかテストする

続いて上記ボタンから「HTML5 Web Notifications Test」に飛び、「Authorize」ボタンをクリックして通知を許可した後、「Show」ボタンをクリックしてダミーの通知を出してテストしよう。

「Show」ボタンをクリックすると下記のような通知が表示されるはず。

この通知は「バナー」ではなく「通知パネル」なのでユーザーが何らかのアクションを起こさない限り画面に留まる。

続いてAutomatorに戻り「JavaScriptを実行」の再生ボタンをクリックして実行する。

実行して先ほど表示させた通知が消えたら成功。

5. クイックアクションを保存する

続いてAutomatorのメニューバーから「保存」をクリックして適当な名前で保存する(ここでは「通知クリア」)。

6. キーボードショートカットを設定する

続いてシステム環境設定の「キーボード」の「ショートカット」の項目を開こう。

左パネルから「サービス(①)」を選択し、右のウィンドウを下までスクロールすると先ほど作成したクイックアクションが表示されているはずなのでチェックを付け、「ショートカットを追加」をクリックして任意のショートカットキーを押して割り当てる。

7. 必要に応じてアプリケーションのアクセシビリティアクセスを許可して完了

続いては設定したショートカットキーを実際に実行して通知をクリアしてみよう。

なお、ショートカットキーを押してクイックアクションを実行すると上記画像のようにショートカットキーを実行した時にアクティブだったアプリケーションをアクセシビリティで許可する必要があるので、上記画面が出たらその都度そのアプリケーションをアクセシビリティに追加し許可する。

Automatorを使用する場合の面倒な点として前述のようにショートカットキーを実行するアプリケーションごとにFinderならFinder、ChromeならChromeといったようにいちいちアプリケーションごとにアクセシビリティで許可を与える必要がある点。

8. 元に戻す場合は.workflowファイルを削除する

元に戻す場合は下記の場所にある「~.workflow」ファイルを削除する。

~/Library/Services

もしくは下記ターミナルコマンドでも可。

sudo rm ~/Library/Services ファイル名.workflow

スクリプトエディタで単一で実行可能なアプリケーションを作って通知を消去する方法

前述のようにAutomatorだとキーボードショートカットを実行するごとにアクセシビリティにその時アクティブだったアプリケーションを追加するのが面倒であるため、手間はかかるがスクリプトエディタ(同じくmacOS付属アプリケーション)とAutomatorを組み合わせて単一のアプリケーションを作成し、そのアプリケーションにキーボードショートカットを割り当てるのも有効。

なお、スクリプトエディタではなくAutomatorのみでも同じことが可能だが、スクリプトエディタの場合はスクリプトをコピペするだけで簡単にアプリケーションが作成可能なのでここではスクリプトエディタとAutomatorの両方を用いている。

1. スクリプトエディタを起動しJavaScriptに切り替える

まずスクリプトエディタを起動する。

左上の「AppleScript」をクリック。

「JavaScript」をクリックして言語を切り替える。

2. JavaScriptを貼り付ける

続いて下記のJavaScriptを貼り付ける(前述のAutomatorで使用したJSと同じ)。

なお、下記スクリプトをスクリプトエディタ上で実行する場合はスクリプトエディタをシステム環境設定の「セキュリティとプライバシー」の「プライバシー」の項目でアクセシビリティに追加し、アクセス許可を与えよう。

下記ボックスはスクロール可。

function run(input, parameters) {

    const notNull = (val) => {
      return val !== null && val !== undefined;
    }
  
    const appName = "";
    const verbose = true;
  
    const CLEAR_ALL_ACTION = "すべて消去";
    const CLOSE_ACTION = "閉じる";
  
    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;
  }

上記スクリプトは管理人が日本語環境用に直しただけなので、改めてスクリプト制作者の方に感謝。

参考 close_notifications_applescript.jsGitHub

3. アプリケーションとして保存する

続いてスクリプトエディタのメニューバーから「保存」をクリックして「ファイルフォーマット」を「アプリケーション」として適当な名前をつけて保存する。

4. 保存したアプリケーションにアクセシビリティ許可を与える

保存したアプリケーションを実行すると初回実行時は上記のような警告が出るので「OK」をクリックしてシステム環境設定の「セキュリティとプライバシー」の「プライバシー」の項目を開く。

作成したアプリケーションにチェックを付けてアクセシビリティへのアクセス許可を与えよう。

5. アプリケーションを実行して通知が消えるかテストする

続いて、改めてアプリケーションを実行して通知が消去されるのを確認しておこう。

現在画面上に通知がないのであれば前述のAutomatorの場合と同じように上記ボタンから「HTML5 Web Notifications Test」に飛び、「Authorize」ボタンをクリックして通知を許可した後、「Show」ボタンをクリックして通知のダミーを表示させることが可能だ。

6. Automatorを起動してクイックアクションを選択

次に作成したアプリケーションをキーボードで実行するためのショートカットをAutomatorで作ろう。

まずAutomatorを起動して新規書類を作り「クイックアクション」をクリックして右下の「選択」をクリック。

なお、まだAutomatorをアクセシビリティに追加してアクセス許可を与えていないのなら前述の手順でAutomatorをアクセシビリティに追加しておこう。

7. 「ワークフローが受け取る現在の項目」を「入力なし」に設定

クイックアクションを選択して上記画面が表示されたらウィンドウ上部の「ワークフローが受け取る現在の項目」をクリック。

一覧から「入力なし」をクリックして選択。

8. アプリケーションを起動のアクションを追加

続いて左パネルから「アプリケーションを起動」のアクションを右のエリアにドラッグして追加(②)。

9. 作成したアプリケーションを選択する

続いて「アプリケーションを起動」のアクションのプルダンメニューをクリック。

プルダウンメニューから「その他」をクリック。

一覧から先ほど作成したアプリケーションをクリックして「選択」をクリック。

10. クイックアクションを保存する

続いてAutomatorのメニューバーの「保存」をクリックしてクイックアクションに適当な名前を付けて保存する。

11. キーボードショートカットを設定して完了

続いてシステム環境設定の「キーボード」の「ショートカット」の項目を開こう。

左パネルから「サービス(①)」を選択し、右のウィンドウを下までスクロールすると先ほど作成したアプリケーション起動用のクイックアクションが表示されているはずなのでチェックを付け、「ショートカットを追加」をクリックして任意のショートカットキーを押して割り当てる。

あとは割り当てたショートカットキーを押して作成したアプリケーションが起動し、画面上の通知が消えるのを確認したら完了。

この方法であればいちいちショートカット実行時にアクティブだったアプリケーションをアクセシビリティに追加する必要がない。

なお、作成したアプリケーションは起動しても処理を終えると自動で終了する。

12. 元に戻す場合は.workflowファイルを削除する

元に戻す場合は下記の場所にある「~.workflow」ファイルを削除する。

~/Library/Services

もしくは下記ターミナルコマンドでも可。

sudo rm ~/Library/Services ファイル名.workflow

まとめ

macOS Big Surは通知センターが刷新されたのに加えて年々強固になるセキュリティのおかげで、サクッとキーボードショートカットで通知を消去といったことができず本記事のような少々面倒な手順を踏む必要があるのが厄介。