LoginSignup
8
1

【WSL2】時間のかかるコマンドが終了したらデスクトップ通知が出るようにした

Last updated at Posted at 2023-12-06

やりたかったこと

ビルドやデプロイなど数分単位でかかる処理を待つのが嫌、だけどターミナルに張り付くのは時間の無駄ということで、自動的に通知してくれる仕組みを作りました。

↓こんな感じ
a.png

動作環境

  • Windows11 Pro 23H2
  • WSL 2.0.9.0
  • Ubuntu 22.04.3 LTS
  • Bash 5.1.16

WSLからWindows側にデスクトップ通知を出す

PowerShellでデスクトップ通知(トースト通知と言うらしい)を簡単に出せます。
スクリプトを書いて、WSLから呼び出す形が良さそうです。
PowerShellで出せる通知の種類やカスタマイズについてはこちらの記事が参考になりました。

実行ポリシー

WSLからPowerShellスクリプトを実行するために実行ポリシーの変更が必要でした。
PowerShellを管理者権限で開き、下記のコマンドを実行します。

Set-ExecutionPolicy RemoteSigned

完成形

WSLから呼び出すことを考慮して作成したスクリプトがこちらです。

notify.ps1
param (
    [string]$message = "Command completed!"
)

[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText01)
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($template.GetXml())
$toastTextElements = $xml.GetElementsByTagName("text")
$toastTextElements.Item(0).AppendChild($xml.CreateTextNode($message)) > $null
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
$appId = "WSL Notification"
$notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId)

$notifier.Show($toast)

実行

WSLからテキストを指定して実行することができます。

powershell.exe -File 'C:\notify.ps1' -message "通知!!"

コマンド終了時に通知する

シェルがBashの場合、~/.bashrcに関数を定義すればコマンドを作成できます。
まず思いついたのが、notify npm run buildのように、実行したい処理の前につけることで動作するコマンドを定義することです。

~/.bashrc
function notify() {
    start=$(date +%s)
    "$@"
    end=$(date +%s)
    duration=$((end - start))
    powershell.exe -File 'C:\notify.ps1' -message "Command $@ completed in $duration seconds"
}

これで要件は満たしているのですが、コマンドの頭に都度notify と付けるのは面倒ですし、意識せずとも勝手に通知を出してほしいです。
そこで、最終的には以下のような実装になりました。

完成形

~/.bashrc
function preexec_invoke_exec() {
    if [[ $- == *i* ]]; then
        this_command=$(history 1 | sed -e 's/^[ ]*[0-9]*[ ]*//g')

        # 無視するコマンドのリスト
        ignore_commands=("vim" "nano" "emacs")

        # 現在のコマンドが無視リストに含まれているかチェック
        for cmd in "${ignore_commands[@]}"; do
            if [[ $this_command == $cmd* ]]; then
                unset timer
                return
            fi
        done

        timer=${timer:-$SECONDS}
    fi
}

function precmd_invoke_exec() {
    if [[ $- == *i* ]] && [[ $timer ]]; then
        runtime=$((SECONDS - timer))
        unset timer

        if [[ $runtime -ge 5 ]]; then
            powershell.exe -File 'C:\notify.ps1' -message "Command '$this_command' completed in $runtime seconds"
        fi
    fi
}

trap 'preexec_invoke_exec' DEBUG
PROMPT_COMMAND='precmd_invoke_exec'

解説

trap 'preexec_invoke_exec' DEBUG
PROMPT_COMMAND='precmd_invoke_exec'

この2行によって、各コマンドの実行前にpreexec_invoke_exec、終了後にprecmd_invoke_exec関数が実行されます。
コマンドの実行前後で時刻を取得することで何秒かかったかを計算し、5秒以上経過していたらnotify.ps1を実行する仕組みです。

    if [[ $- == *i* ]]; then

インタラクティブに実行されたコマンドのみを通知の対象としています。

        for cmd in "${ignore_commands[@]}"; do
            if [[ $this_command == $cmd* ]]; then
                unset timer
                return
            fi
        done

エディタでファイル編集している時間なども計測対象になることに気付いたので、特定のコマンドを無視するようにしました。
あまりスマートな解決方法ではありませんが…

まとめ

ちょっとしたスクリプトですが、無駄な時間を減らせて地味に便利だと思います

8
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1