やりたかったこと
ビルドやデプロイなど数分単位でかかる処理を待つのが嫌、だけどターミナルに張り付くのは時間の無駄ということで、自動的に通知してくれる仕組みを作りました。
動作環境
- 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から呼び出すことを考慮して作成したスクリプトがこちらです。
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
のように、実行したい処理の前につけることで動作するコマンドを定義することです。
function notify() {
start=$(date +%s)
"$@"
end=$(date +%s)
duration=$((end - start))
powershell.exe -File 'C:\notify.ps1' -message "Command $@ completed in $duration seconds"
}
これで要件は満たしているのですが、コマンドの頭に都度notify
と付けるのは面倒ですし、意識せずとも勝手に通知を出してほしいです。
そこで、最終的には以下のような実装になりました。
完成形
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
エディタでファイル編集している時間なども計測対象になることに気付いたので、特定のコマンドを無視するようにしました。
あまりスマートな解決方法ではありませんが…
まとめ
ちょっとしたスクリプトですが、無駄な時間を減らせて地味に便利だと思います