PythonでリッチなCUIアプリケーションを作ってみた

## はじめに

Pythonには、CUIアプリケーションを作成するための様々なライブラリがある。その中でも、Richライブラリは、美しく洗練されたCUIアプリケーションを作成するための強力なツールである。

なんとなくリッチなCUIに興味があったから触ってみたが、長い処理を行っている場合などは、使うとユーザーフレンドリーでめちゃよさそうだと感じた。

本記事では、Richライブラリを使用して、タスク管理ツールを作成する過程を紹介する。

## Richライブラリの概要

Richライブラリは、CUIアプリケーションに色、スタイル、および高度なフォーマットを追加するためのPythonライブラリである。主な機能には以下のようなものがある。

- カラフルな出力
- テーブルの作成
- プログレスバー
- シンタックスハイライト
- マークダウンのレンダリング

Richライブラリを使用することで、ユーザーフレンドリーで視覚的に優れたCUIアプリケーションを簡単に作成できる。

## タスク管理ツールの作成

### GitHubのコード

https://github.com/koinunopochi/python-s_rich_cli

### 基本機能の実装

まず、タスク管理ツールの基本機能を実装する。タスクを追加、削除、および一覧表示する機能を追加する。

```python
from rich import print
from rich.console import Console
from rich.table import Table
from rich.prompt import Prompt

tasks = []

def display_tasks():
    table = Table(title="タスク一覧")
    table.add_column("ID", style="cyan")
    table.add_column("タスク名", style="magenta")
    table.add_column("優先度", style="green")

    for i, task in enumerate(tasks, start=1):
        priority = "高" if task["priority"] == "high" else "中" if task["priority"] == "medium" else "低"
        table.add_row(str(i), task["name"], priority)

    console = Console()
    console.print(table)

def add_task():
    name = Prompt.ask("タスク名を入力してください")
    priority = Prompt.ask("優先度を入力してください", choices=["high", "medium", "low"])
    tasks.append({"name": name, "priority": priority})
    print("[bold green]タスクが追加されました[/bold green]")

def delete_task():
    task_id = Prompt.ask("削除するタスクのIDを入力してください", default="0")
    if task_id.isdigit() and 1 <= int(task_id) <= len(tasks):
        tasks.pop(int(task_id) - 1)
        print("[bold green]タスクが削除されました[/bold green]")
    else:
        print("[bold red]無効なタスクIDです[/bold red]")
```

:::note info
`Table`クラスを使用して、タスクの一覧表示を見やすくフォーマットしている。また、`Prompt`クラスを使用して、ユーザー入力を簡単に処理している。
:::

### スピナーの追加

処理中にスピナーを表示することで、ユーザーに処理の進行状況を伝えることができる。ここでは、Haloライブラリを使用してスピナーを追加する。

```python
from halo import Halo

def add_task(console):
    name = Prompt.ask("タスク名を入力してください")
    priority = Prompt.ask("優先度を入力してください", choices=["high", "medium", "low"])
    with console.status("[bold green]タスクを追加中..."):
        tasks.append({"name": name, "priority": priority})
    print("[bold green]タスクが追加されました[/bold green]")

def delete_task(console):
    task_id = Prompt.ask("削除するタスクのIDを入力してください", default="0")
    with console.status("[bold green]タスクを削除中..."):
        if task_id.isdigit() and 1 <= int(task_id) <= len(tasks):
            tasks.pop(int(task_id) - 1)
            print("[bold green]タスクが削除されました[/bold green]")
        else:
            print("[bold red]無効なタスクIDです[/bold red]")
```

:::warn
スピナーを表示しながらユーザー入力を受け付けるのは、少し工夫が必要である。単純にHaloのコンテキストマネージャー内でPromptを使用すると、入力プロンプトが表示されなくなってしまう。
:::

### ヘルプ機能の追加

ユーザーがツールの使い方を理解しやすいように、ヘルプ機能を追加する。

```python
def display_help():
    help_text = """
    タスク管理ツール - 使い方

    このツールでは、以下のアクションが可能です:
    1. タスクを追加: 新しいタスクを追加します。タスク名と優先度を入力してください。
    2. タスクを削除: 既存のタスクを削除します。削除するタスクのIDを入力してください。
    3. 終了: ツールを終了します。

    アクション番号を入力し、Enterキーを押してください。
    """
    print(help_text)
```

## 完成したコード

```python
from rich import print
from rich.console import Console
from rich.table import Table
from rich.prompt import Prompt
from halo import Halo

tasks = []

def display_tasks():
    table = Table(title="タスク一覧")
    table.add_column("ID", style="cyan")
    table.add_column("タスク名", style="magenta")
    table.add_column("優先度", style="green")

    for i, task in enumerate(tasks, start=1):
        priority = "高" if task["priority"] == "high" else "中" if task["priority"] == "medium" else "低"
        table.add_row(str(i), task["name"], priority)

    console = Console()
    console.print(table)

def add_task(console):
    name = Prompt.ask("タスク名を入力してください")
    priority = Prompt.ask("優先度を入力してください", choices=["high", "medium", "low"])
    with console.status("[bold green]タスクを追加中..."):
        tasks.append({"name": name, "priority": priority})
    print("[bold green]タスクが追加されました[/bold green]")

def delete_task(console):
    task_id = Prompt.ask("削除するタスクのIDを入力してください", default="0")
    with console.status("[bold green]タスクを削除中..."):
        if task_id.isdigit() and 1 <= int(task_id) <= len(tasks):
            tasks.pop(int(task_id) - 1)
            print("[bold green]タスクが削除されました[/bold green]")
        else:
            print("[bold red]無効なタスクIDです[/bold red]")

def display_help():
    help_text = """
    タスク管理ツール - 使い方

    このツールでは、以下のアクションが可能です:
    1. タスクを追加: 新しいタスクを追加します。タスク名と優先度を入力してください。
    2. タスクを削除: 既存のタスクを削除します。削除するタスクのIDを入力してください。
    3. 終了: ツールを終了します。

    アクション番号を入力し、Enterキーを押してください。
    """
    print(help_text)

def main():
    console = Console()

    while True:
        display_tasks()
        print("\n[bold blue]アクション:[/bold blue]")
        print("1. タスクを追加")
        print("2. タスクを削除")
        print("3. 終了")
        print("4. ヘルプ")

        def prompt_with_spinner():
            spinner = Halo(text="\nアクション番号を選択してください", spinner={
                "interval": 120,
                "frames": [
                    "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"
                ]
            })
            spinner.start()
            action = ""
            while action not in ["1", "2", "3", "4"]:
                action = input()
                print(action)  # 入力文字をエコーバックする
            spinner.stop()
            return action

        action = prompt_with_spinner()

        if action == "1":
            add_task(console=console)
        elif action == "2":
            delete_task(console=console)
        elif action == "3":
            print("[bold green]タスク管理ツールを終了します[/bold green]")
            break
        elif action == "4":
            display_help()

if __name__ == "__main__":
    main()
```

## まとめ

本記事では、Pythonのrichライブラリを使用して、タスク管理ツールを作成する過程を紹介した。Richライブラリを使用することで、美しく洗練されたっぽいCUIアプリケーションを簡単に作成できる。

また、Haloライブラリを使用してスピナーを追加し、処理中の視覚的なフィードバックを提供することで、ユーザー体験を向上させることができる。

また、`pip install` などの動きを作成している背景を想像でき、面白いと感じた。

:::note alert
スピナーとユーザー入力を同時に扱うには、工夫が必要である。本記事では、その問題に対する完全な解決策を提示することはできなかったが、今後の課題としたい。
:::

簡単に導入が可能であるので、自分のプロジェクトにRichライブラリを導入して、ユーザーフレンドリーで美しいCUIアプリケーションを作成しようと思う。

また、ほかの言語でも必ずライブラリがあると感じたので、業務でよく使うNodeなどは改めて触ってみようと思う。