Windowsターミナルと、エクスプローラーを開くスクリプト

# はじめに
Node.jsにしてもPythonにしても、ターミナル(コマンドプロンプト)とエクスプローラーを開いて、そこからVisualStudio Codeで作業を始める方は多いと思います。
いつも同じフォルダに`cd`したり、エクスプローラーで開いたりします。

めんどくさいのでそこまでやるツールを作りたいんですが、Win11になって正式に現れたターミナルにしても、エクスプローラーにしても、コマンドラインがあるのかないのかわかりませんが公開はされていないみたい。

以前、Windows10時代に作った自分の記事を見ながら、ここでもう一度作ったので、そのメモです。
下記はWindows10のときの記事。

https://qiita.com/yo16/items/d17b3cec2302bf8e0713

説明の仕方として、最初に、１つ１つの技法を紹介して、最後にまとめて、今回のスクリプトを紹介します。技法っていうほどのこともないけど。


# 技法
## ps1ファイル
WindowsのPowerShellを使いたいので、新規テキストファイルで空のファイルを作って、拡張子を.ps1にします。私は、初期設定.ps1にしました。

これを右クリックから「編集」で、専用のエディタも出るので、ここで編集します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/87b268ab-8362-33fd-2173-8a099649fb0d.png)

実行は、ツールバーにある「スクリプトを実行」ボタン（再生ボタン）でいけます。このエディタの使い方は割愛します。試行錯誤してみてください。

試しに、下記を書いてみると、下のコンソールの画面に"hello world!"が出ます。
```powershell
Write-Output "hello world!"
```
`Write-Output`は、Pythonでいう`print`、JavaScriptでは`console.log`の、標準出力です。

ちなみに、編集はメモ帳でもよくて、私はメモ帳で書くことよくもあります。その場合の実行は、ファイルを右クリックして「PowerShellで実行」です。


## 変数
PowerShellの変数は、`$xxx`と、`$`から始まる文字です。そこに代入し、読むときは`$xxx`を呼び出します。

文字列の結合は`+`です。

それを組み合わせて、こんな感じ。
```powershell
$hello = "hello"
$world = "world"
$message = $hello + " " + $world + "!"
Write-Output $message
# hello world!
```
難しい使い方は、今回しません。


## ターミナルとエクスプローラーを起動
起動するコマンドはこれです。
```powershell
# ターミナル（PowerShell）
Start-Process wt.exe
# エクスプローラー
Start-Process explorer.exe
```
`wt.exe`は、Windows Terminalの実体名（exeの名前）で、頭文字からとられています。覚えよう。

エクスプローラーは、癖のあるオプションもあるのですが、今回は使うのはやめました。素のエクスプローラーが立ちあがります。

https://learn.microsoft.com/ja-jp/previous-versions/powershell/module/microsoft.powershell.management/start-process?view=powershell-7.3


## sleep
ちょっと時間を置く処理です。
`sleep -Milliseconds`で、ミリ秒単位でスリープします。
```powershell
Write-Output "1"
sleep -Milliseconds 1000
Write-Output "2"
```
これは、PowerShellが動くの速度に比べて、エクスプローラーなどのGUIの速度は遅いので、PowerShellがコマンドをバババッと送ると、空振りしちゃうことがあります。その場合、スリープしてちょっと間を開けるとうまくいくことがあるので、使っています。

本来は、コマンドを送った後、「終わったよ～」という返事を待って、次のコマンドを送るとやりたいです。実は`Start-Process`で、-Wait


## コマンドを送る仕組み
急にプログラミングっぽい話です。

今作っているps1のファイルが実行されているウィンドウから、新たにターミナルとエクスプローラーを開き、そこへコマンドを送り付けるので、その仕組みです。

プロセスIDが`$processId`という変数に入っている前提で、それをアクティブにして（＝windowsの最前面に持ってくる）、それから`abcde`というキーを順番に押す、というコードが下記です。
```powershell
Add-Type -AssemblyName Microsoft.VisualBasic
Add-Type -AssemblyName System.Windows.Forms

# ウィンドウをアクティブにする
[Microsoft.VisualBasic.Interaction]::AppActivate($processId)

# 今のトップウィンドウにキーを押したことを送る
[System.Windows.Forms.SendKeys]::SendWait("abcde")
```
プロセスIDってなんやねん！ということですが、タスクマネージャーのPIDの列に表示されているIDです。下記は、メモ帳＝Notepad.exeを起動した状態のタスクマネージャーですが、メモ帳のプロセスIDが19476であることを示しているので、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/3617de28-7235-b3da-fd1e-15a4b40bc76a.png)

こんな風に直で`19476`と書くと、メモ帳がアクティブになって、メモ帳にabcdeって書かれます。
```powershell
Add-Type -AssemblyName Microsoft.VisualBasic
Add-Type -AssemblyName System.Windows.Forms

$processId = 19476    ### ←追加
# ウィンドウをアクティブにする
[Microsoft.VisualBasic.Interaction]::AppActivate($processId)

# 今のトップウィンドウにキーを押したことを送る
[System.Windows.Forms.SendKeys]::SendWait("abcde")
```

これによって、ウィンドウにキー操作を送る、つまりコマンドを送ることができます。
これだけでも便利ツールになる場面がありそうですね。

Ctrlキーを押しながらTの場合は`(^t)`とか、Enterキーは`{ENTER}`など、特殊なことがいくつかあります。具体的には下記をお読みください。

https://learn.microsoft.com/ja-jp/previous-versions/windows/scripting/cc364423(v=msdn.10)?redirectedfrom=MSDN


## ちょっと戻って、ターミナルとエクスプローラーを起動
あえて戻りますが、もう一度、ターミナルとエクスプローラーを起動する話です。

なんと起動するときに、戻り値としてプロセスIDをゲットできます。
```powershell
Add-Type -AssemblyName Microsoft.VisualBasic
Add-Type -AssemblyName System.Windows.Forms

# ターミナル（PowerShell）を起動
$process = Start-Process wt.exe -PassThru

# １秒くらい待つ
sleep -Milliseconds 1000

# ウィンドウをアクティブにする
[Microsoft.VisualBasic.Interaction]::AppActivate($process.Id)
```
こうするとプロセスIDを利用できて、アクティブにできます。

でも大きな問題があって、実は**エクスプローラーはこれができません**。
エクスプローラーは、Start-Processで起動すると、起動したプロセスは、別のプロセスを起動したらすぐ終了してしまいます。起動された別のプロセスが、普通に触るエクスプローラー。なので別IDになるとのこと。

https://stackoverflow.com/questions/12084221/how-to-get-the-process-id-of-explorer-launched-from-powershell

プロセスIDを得るための対策は、こんな感じです。
```powershell
# エクスプローラーを起動
Start-Process explorer.exe
$process = ps | ? {$_.Name -eq "explorer"} | sort -Property CPU -Descending | select -First 1

# ウィンドウをアクティブにする
[Microsoft.VisualBasic.Interaction]::AppActivate($process.Id)
```
起動したら"explorer"で全プロセスを探し、複数出てくるから、今一番動いてるものを使う、という技です。

ここでもう１つどんでん返し。

せっかく正しいプロセスIDを得ても、`AppActivate`すると**かえってフォーカスが外れてしまって**、この後の`SendKeys`が渡らなくなります。どうやら、エクスプローラーは階層的になっているらしく、ここで得たIDを指定すると１つ上のもののようです。
下記の図はタスクマネージャーの一部です。4472は"ホーム"と表示されるウィンドウのもう１つ上っぽいですよね。なのでもう、Activateすることはせず、起動しっぱなしにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/18455339-3b0d-fe07-2d7d-a673ea6f7677.png)

業務アプリだったらこんな方法はしたくないけど、自分の便利ツールくらいならまぁいいかという感じ。でもとても気持ち悪いので、いい方法があったら教えてください。


## ターミナルでタブ名を変更
ターミナルは、タブをダブルクリックすれば名前を変更できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/395f09b1-8a1e-05c0-ec36-ee6dac420f4a.png)

普段これで名前を変えていますが、自動で変更したいので、PowerShellのコマンドで変更する必要があります。そのコマンドが下記。
```powershell
$Host.ui.RawUI.WindowTitle="abc"
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/d8457432-4b77-de1f-fd86-c6e7ef92f43b.png)

大したことではなく、そうやって変えるんだ、ふーん、という話でした。でも覚えられないですね。

ちなみにコマンドプロンプトの場合は、`title abc`です。覚えやすい。


# ターミナルとエクスプローラーを開くツール
さて、上記で紹介した手法を使って、ターミナルとエクスプローラーを開くツールを作ってみます。

要件を整理します。
- 【概要】
    - クライアント・サーバー型のアプリケーションをコーディングするために、クライアントとサーバーの両方の環境を準備する
    - クライアント、サーバーともにNode.js
    - 今はクライアントを開発中なので、クライアントはgit用の環境もほしい
- 【具体】
    - ターミナルで、３つのタブを開く
        - (1) クライアントのgit環境
        - (2) クライアントのNode.js環境、`npm run dev`を実行
        - (3) サーバーのNode.js環境、`npm run dev`を実行
    - エクスプローラーで、２つのタブを開く
        - (1) クライアント
        - (2) サーバー

## ターミナルで、３つのタブを開く
コードを貼ります。

あえて関数化せず、べたべた書いてます。
```powershell
# 設定
# クライアントのgitフォルダ
$client_git_path = "D:\Public\github\yo16\sql_parse_client_202407"

# クライアントのnpmフォルダ
$client_npm_path = "D:\Public\github\yo16\sql_parse_client_202407\sql_visualizer"

# サーバーのnpmフォルダ
$server_npm_path = "D:\Public\github\yo16\sql_parse_server_202407\sql_parse_ts"

# ----------------------------------
# インポート
Add-Type -AssemblyName Microsoft.VisualBasic
Add-Type -AssemblyName System.Windows.Forms

# ターミナル
# 起動
$terminalProcess = Start-Process PowerShell.exe -PassThru
#Write-Output $terminalProcess
sleep -Milliseconds 1000
[Microsoft.VisualBasic.Interaction]::AppActivate($terminalProcess.Id)

# クライアント git
$cmd_str = "cd " + $client_git_path + "{ENTER}"
[System.Windows.Forms.SendKeys]::SendWait($cmd_str)
[System.Windows.Forms.SendKeys]::SendWait("`$Host.ui.RawUI.WindowTitle=""GIT""{ENTER}")  # タイトルを変更
[System.Windows.Forms.SendKeys]::SendWait("git status{ENTER}")

# クライアント npm
# Shift+Ctrl+1で、新しいタブを開く
[System.Windows.Forms.SendKeys]::SendWait("(^+1)")
sleep -Milliseconds 1000
$cmd_str = "cd " + $client_npm_path + "{ENTER}"
[System.Windows.Forms.SendKeys]::SendWait($cmd_str)
[System.Windows.Forms.SendKeys]::SendWait("`$Host.ui.RawUI.WindowTitle=""client""{ENTER}")  # タイトルを変更
[System.Windows.Forms.SendKeys]::SendWait("npm run dev{ENTER}")


# サーバー npm
# Shift+Ctrl+1で、新しいタブを開く
[System.Windows.Forms.SendKeys]::SendWait("(^+1)")
sleep -Milliseconds 1000
$cmd_str = "cd " + $server_npm_path + "{ENTER}"
[System.Windows.Forms.SendKeys]::SendWait($cmd_str)
[System.Windows.Forms.SendKeys]::SendWait("`$Host.ui.RawUI.WindowTitle=""server""{ENTER}")  # タイトルを変更
[System.Windows.Forms.SendKeys]::SendWait("npm run dev{ENTER}")
```
同じようなことの繰り返しなので、関数化したほうがいいかもしれません。でも使いまわすようなこともないし、特有なことが起こりがちなので、私はこの手のツールの場合は、結構べた書きです。

ここまでで、ターミナルの３つのタブで、３つの環境（フォルダ）にいる、ということができました。

## エクスプローラーで、２つのタブを開く
つぎにエクスプローラーです。
実際は、上のターミナル関係のコードの下に書きますので、設定とインポートの部分は書きません。

```powershell
#～～～ ここより上は省略 ～～～

# エクスプローラー
# 起動
Start-Process explorer.exe
sleep -Milliseconds 1000

# クライアントのnpmフォルダを開く
[System.Windows.Forms.SendKeys]::SendWait("{F4}")
$cmd_str = $client_npm_path + "{ENTER}"
[System.Windows.Forms.SendKeys]::SendWait($cmd_str)

# 新しいタブを開く
[System.Windows.Forms.SendKeys]::SendWait("(^t)")
sleep -Milliseconds 1000

# サーバーのnpmフォルダを開く
[System.Windows.Forms.SendKeys]::SendWait("{F4}")
$cmd_str = $server_npm_path + "{ENTER}"
[System.Windows.Forms.SendKeys]::SendWait($cmd_str)
```
エクスプローラーのところで書いた通り、sleepはするけど、アクティブの操作は無視です。最上位に出ているはず！という願いにも似た前提で。

あとは、Ctrl+Tで新しいタブを開く点と、F4を押すとフォルダを直で入力できるようになるので入力する点。


以上で解説おしまいです。

# おわりに
エクスプローラーの周辺が混とんとしていますね。最終的には、Activateするのをあきらめて、最前面に出ていることを願うという大技に出ました。:rolling_eyes: これはよくない。それが成り立つならターミナルもActivateしなくていいし。

まぁ、sleepすることも本当は気持ち悪いです。1秒待てば必ず使える状態になっているのかというとそうとは限らないわけで。

そういうわけで今回は、すっきりしない点がありますが、自分用の時短ツールだと思えば、そんなにカリカリせず許してやろうという気持ちにもなります。この記事を参考にご自分のツールを作られる方も、それくらいの気持ちでお使いください。

個人的には、ないよりはあった方がよいです。ちょっと時間を置くと、フォルダとか`npm run dev`なのか`npm run debug`なのかとか、忘れちゃうし。

ではよき時短ツールライフを。


----
2024/9/23追記

ターミナルも、Activateすると、そんなID知らんって言われました。必ずそうなるのかな。昨日は気づかなかったけど。

その場合はこちらも、Activateせず、起動しっぱなしにすればよいです。