dialoguerライブラリを使ってGitコマンドのCLIツールを作成する方法

## TL;DR

この記事では、[dialoguer](https://docs.rs/dialoguer/latest/dialoguer/index.html)というRustのライブラリを使って、Gitコマンドを実行するためのCLIツールを作成する方法を紹介します。

dialoguerは、Rustでインタラクティブなコマンドラインツールを作成するためのライブラリです。このライブラリを使うことで、非常に簡単な実装ではあるものの比較的にUXの良いCLIツールを作成することができると感じました。

## 作ったもの

https://github.com/makinzm/git-wizard

「[git-wizard](https://github.com/makinzm/git-wizard/tree/main)」は、Gitコマンドを実行するためのインタラクティブなCLIツールです。ユーザーはツールのメニューから実行したいGitコマンドを選択し、簡単な操作でGitの基本的なコマンドを実行できます。例えば、`git add`、`git commit`、`git push`、`git status`、`git branch` などの操作を手軽に行うことができます。

- 動画はこちら：https://github.com/user-attachments/assets/d401dcab-03eb-4fb2-a52c-581a641b602c

### Vim風の操作感

dialoguerを使うと自動的にVimの使用感を持つCLIツールを作成することができます。例えば、`j`を押すと下に移動し、`k`を押すと上に移動するなど、Vimのキーバインドに似た操作が可能です。

- 具体コード：
    - j,k: https://github.com/console-rs/dialoguer/blob/6244c77656e8f3e934a2981c5c7e9dacf938a492/src/prompts/fuzzy_select.rs#L283-L317
    - q: https://github.com/console-rs/dialoguer/blob/6244c77656e8f3e934a2981c5c7e9dacf938a492/src/prompts/fuzzy_select.rs#L272-L279

このあたりの実装がcrateの中で行われているのが、とても使い勝手がよく素晴らしいと感じました。

## 作ったもののコード

### メイン部分

`main` 関数では、ユーザーに対してコマンドの選択肢を表示し、その選択に応じて適切なGitコマンドを実行します。

```rust
fn main() {
    let options = vec![
        "ステージング (git add)", 
        "コミット (git commit)", 
    ];

    let term = Term::stderr();
    println!("Git Wizardへようこそ！\n");

    loop {
        // メニューの選択
        let selection = Select::with_theme(&ColorfulTheme::default())
            .with_prompt("実行したいGitコマンドを選んでください (:q で終了)")
            .items(&options)
            .default(0)
            .interact_on_opt(&term)
            .unwrap();

        match selection {
            Some(index) => match index {
                0 => {
                    // git add
                    let files = user_input_with_exit(&term, "ステージングしたいファイル/ディレクトリを指定してください (例: .)");
                    execute_command("git", &["add", &files]);
                }
                1 => {
                    // git commit
                    let message = user_input_with_exit(&term, "コミットメッセージを入力してください");
                    execute_command("git", &["commit", "-m", &message]);
                }
                _ => {
                    println!("不明な選択肢です。");
                }
            },
            None => {
                // 選択をキャンセル（Ctrl+CやESCが押された場合）
                println!("終了します。");
                break;
            }
        }
    }
}
```

ユーザーが選択したコマンドに応じて、対応するGitコマンドを実行を行います。

### ユーザー入力の取得

`user_input_with_exit` 関数では、`:q` と入力された場合に終了する機能を備えつつ、ユーザーからの入力を受け付けます。

```rust
fn user_input_with_exit(term: &Term, prompt: &str) -> String {
    loop {
        let input: String = Input::new()
            .with_prompt(prompt)
            .allow_empty(true)
            .interact_text()
            .unwrap();

        if input.trim() == ":q" {
            println!("終了します。");
            term.clear_last_lines(1).unwrap();
            std::process::exit(0);
        }

        if !input.trim().is_empty() {
            return input;
        }
    }
}
```

### コマンドの実行

`execute_command` 関数は、指定されたコマンドを実行し、その結果を標準出力や標準エラーに表示します。

```rust
fn execute_command(command: &str, args: &[&str]) {
    println!("\n実行中: {} {}\n", command, args.join(" "));
    let output = Command::new(command)
        .args(args)
        .output();

    match output {
        Ok(output) => {
            if !output.stdout.is_empty() {
                println!("{}", String::from_utf8_lossy(&output.stdout));
            }
            if !output.stderr.is_empty() {
                eprintln!("{}", String::from_utf8_lossy(&output.stderr));
            }
        }
        Err(e) => {
            eprintln!("コマンドの実行に失敗しました: {}", e);
        }
    }
}
```

## 最後に

RustのCLIツールを初めて作成したのですが、dialoguerを使うことで非常に簡単にインタラクティブなCLIツールを作成することができました。また、Vim風の操作感を持つことで、より使いやすいツールを作成することができたと感じました。

今までPythonやGoでCLIツールを作成していましたが、RustにもCLIツールを作成するための優れたライブラリがあることを知り、今後もRustを使ってCLIツールを作成していきたいと思います。