PowerShellで自分だけのオリジナルステータスバーを作る
PowerShellでこんな感じ↓のステータスバーを作る方法をご紹介します。
Windows Terminal上で動作していますが、他のターミナルアプリケーション、またLinux/macOSでも同様にカスタマイズが可能です。
本記事の英語版はこちら。
背景
普段はWindowsで作業をすることが多く、シェルはPowerShellをメインで使用しています。Oh My Posh などでPowerShellのプロンプトをカスタマイズされている方は多いかと思いますが、その中で現在時刻やCPUの使用率を表示するセグメントを見かけることがあります。これらはプロンプトのテキストなので、当然コマンドを実行したりEnterを押してプロンプトを更新させないとその表示内容も更新されません。ターミナルのステータスバー(tmuxのstatus lineやiTerm2のStatus Bar)のように、リアルタイムに情報を更新することはできないものか?と思い立ち、いくつかテスト実装を行いました。
ところが、コンソールバッファはユーザ操作によっていつでも書き換わる可能性があるため、非同期にこれを書き換えることは容易ではありません。試行錯誤した結果、ユーザ操作の影響を受けずに安全に情報を書き換えられ、かつPowerShellからアクセス可能な場所を見つけました。コンソールのタイトル領域です。
ここでは、ターミナルのタイトルをステータスバーのようにカスタマイズする方法をご紹介します。
DynamicTitle モジュール
PowerShellでは以下のようにしてコンソールのタイトルを設定することができます。
$host.UI.RawUI.WindowTitle = 'This is a title'
ターミナル上でのユーザ操作と並行してリアルタイムに表示内容を更新するためには、このコードをバックグランウドスレッドからある時間間隔で定期的に呼び出さなければなりません。スレッド管理やスレッド間のデータの受け渡し、タイマー処理などが必要になるため、これらを手軽に扱えるよう DynamicTitle というPowerShellモジュールを作成しました。
以下のコマンドでモジュールをインストールすると、
Install-Module -Name DynamicTitle -Scope CurrentUser
ワンライナーでリアルタイムの時刻を表示することができるようになります。
Start-DTTitle {Get-Date}
パラメータで指定されたスクリプトブロックはバックグラウンドスレッドで定期的に実行され、スクリプトブロックの出力がタイトルとして設定されます。
スクリプトブロックが利用できるため、PowerShellからアクセス可能なものは基本的にどんな情報でも表示することができます。メインスレッド(ユーザによるターミナル操作)をブロックすることもありません。
🌿 Gitステータス
はじめに、gitリポジトリの現在のブランチ名と変更のあるファイルの数を表示してみます。
gitのステータスを取得するにはまず、コンソールのカレントディレクトリを知る必要があります。Start-DTTitle
に渡されたスクリプトブロックは裏のスレッド(Runspace)で実行されるため、プロンプトに表示されるものとは別の独自のカレントディレクトリを保持しています。メインスレッドからこのDynamicTitleのスレッドにカレントディレクトリを渡す必要があるわけですが、モジュールではこのようなデータ転送のためにいくつかのジョブを用意しています。
PromptCallback
ジョブでは、PowerShellのPrompt関数が実行される直前(通常このタイミングでカレントディレクトリも変化する)にメインスレッドで呼び出されるスクリプトブロックを登録することができます。スクリプトブロックが返すオブジェクトはスレッドセーフな関数、Get-DTJobLatestOutput
で取得することができます。
$promptCallback = Start-DTJobPromptCallback {
(Get-Location).Path
}
Start-DTTitle {
param($promptCallback)
$currentDir = Get-DTJobLatestOutput $promptCallback
$currentDir
} -ArgumentList $promptCallback
カレントディレクトリが取得できたら、あとはgitステータスの文字列を生成するのみです。
Start-DTTitle {
param ($promptCallback)
$currentDir = Get-DTJobLatestOutput $promptCallback
if (-not $currentDir) {
return
}
Set-Location $currentDir
$branch = git branch --show-current
if ($LastExitCode -ne 0) {
# not a git repository
return '📂{0}' -f $currentDir
}
if (-not $branch) {
$branch = '❔'
}
$gitStatusLines = git --no-optional-locks status -s
$modifiedCount = 0
$unversionedCount = 0
foreach ($line in $gitStatusLines) {
$type = $line.Substring(0, 2)
if (($type -eq ' M') -or ($type -eq ' R')) {
$modifiedCount++
}
elseif ($type -eq '??') {
$unversionedCount++
}
}
$currentDirName = Split-Path $currentDir -Leaf
'📂{0} 🌿[{1}] ✏️{2}❔{3}' -f $currentDirName, $branch, $modifiedCount, $unversionedCount
} -ArgumentList $promptCallback
gitステータスの取得は非同期に実行されるため、リポジトリが巨大で長時間かかったとしてもプロンプト表示が遅くなることはありません。
⌚ コマンドタイマー
プロンプトのカスタマイズで直前のコマンドの実行時間を表示している例を見かけたことがあるかもしれません。プロンプトでは、コマンドの実行終了後に何秒かかったのかを表示することしかできませんが、非同期処理の場合はコマンド実行中に今何秒経過しているかを表示することができます。
CommandPreExecutionCallback
ジョブを使用すると、コマンドが実行される直前にメインスレッドで呼び出されるスクリプトブロックを登録することができます。このジョブによってコマンドの開始時刻を取得することができます。コマンドの終了時刻は先ほどのPromptCallback
ジョブを使って取得します。
$commandStartJob = Start-DTJobCommandPreExecutionCallback {
param($command)
(Get-Date), $command
}
$commandEndJob = Start-DTJobPromptCallback {
Get-Date
}
Start-DTTitle {
param($commandStartJob, $commandEndJob)
$commandStartDate, $command = Get-DTJobLatestOutput $commandStartJob
$commandEndDate = Get-DTJobLatestOutput $commandEndJob
if ($null -ne $commandStartDate) {
if (($null -eq $commandEndDate) -or ($commandEndDate -lt $commandStartDate)) {
$commandDuration = (Get-Date) - $commandStartDate
$isCommandRunning = $true
} else {
$commandDuration = $commandEndDate - $commandStartDate
}
}
if ($command) {
$command = $command.Split()[0]
}
$status = '🟢'
if ($commandDuration.TotalSeconds -gt 1) {
$commandSegment = '[{0}]-⌚{1}' -f $command, $commandDuration.ToString('mm\:ss')
if ($isCommandRunning) {
$status = '🟠'
}
}
'{0} {1}' -f $status, $commandSegment
} -ArgumentList $commandStartJob, $commandEndJob
📈 CPU使用率とネットワーク速度
CPU使用率やネットワーク通信速度などのシステム情報もPowerShellで容易に取得することができるため、ステータスバーによくある表示を作ることも可能です。
この例ではもう一つのJobオブジェクトであるBackgroundThreadTimer
を使用しています。BackgroundThreadTimer
ジョブは独自のスレッドを作成し、そのスレッド上で指定されたスクリプトブロックを一定間隔で呼び出します。処理の完了に長時間を要し、何もしないと他の項目の更新をブロックしてしまうようなものへの利用を想定しています。例えばこの例ではsystemInfoJob
が長時間戻ってこなかったとしても、日付のテキストの更新を阻害することがありません。
$systemInfoJob = Start-DTJobBackgroundThreadTimer -ScriptBlock {
$cpuUsage = (Get-Counter -Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue
$netInterface = (Get-CimInstance -class Win32_PerfFormattedData_Tcpip_NetworkInterface)[0]
$cpuUsage, ($netInterface.BytesReceivedPersec * 8)
} -IntervalMilliseconds 1000
Start-DTTitle {
param($systemInfoJob)
$cpuUsage, $bpsReceived = Get-DTJobLatestOutput $systemInfoJob
$date = Get-Date -Format 'MMM dd HH:mm:ss'
'📆 {0} 🔥CPU:{1:f1}% 🔽{2}Mbps' -f $date, [double]$cpuUsage, [Int]($bpsReceived/1MB)
} -ArgumentList $systemInfoJob
おわりに
PowerShellのコンソールタイトル領域を非同期に設定することで、リアルタイムに様々な情報を表示することができます。Windows Terminalを含めモダンなターミナルであれば、カラー絵文字もきれいにレンダーしてくれるので、プロンプト文字列と同じようにカスタマイズして楽しむ領域としてとらえるのも面白いかもしれません。
Discussion