prompt-toolkitを徹底解説!高機能な対話型インターフェースをPythonで実現 ✨

プログラミング

はじめに: prompt-toolkitとは? 🤔

prompt-toolkit は、Pythonで強力な対話型のコマンドラインインターフェース(CLI)やターミナルアプリケーションを構築するためのライブラリです。標準の input() 関数やGNU Readlineライブラリの単なる代替品にとどまらず、それをはるかに超える豊富な機能を提供します。

なぜ prompt-toolkit を使うのでしょうか? 従来のシンプルなCLIは、ユーザーエクスペリエンスの点で限界がありました。入力補完がなかったり、シンタックスハイライトが効かなかったり、複数行の編集が面倒だったりと、開発者にとっても利用者にとっても使いにくい場面がありました。prompt-toolkit は、これらの課題を解決し、まるで最新のIDEやテキストエディタのようなリッチな操作感をターミナル上で実現します。これにより、CLIアプリケーションの使いやすさが劇的に向上します🚀。

主な特徴をいくつか挙げてみましょう:

  • シンタックスハイライト: 入力中のテキストをリアルタイムで色付け。PythonコードやSQL、HTMLなど、様々な言語に対応可能(Pygmentsライブラリ連携)。
  • 高度な自動補完: 単語の補完はもちろん、文脈に応じた補完候補の表示、ファジー検索によるあいまい補完も可能。
  • 入力履歴: 過去に入力したコマンドを簡単に呼び出し、再利用。ファイルへの永続化も可能。
  • 入力検証: 入力された値が正しい形式かリアルタイムでチェック。
  • キーバインディング: Emacs風、Vi風のキー操作をサポート。カスタムキーバインディングの定義も自由自在。
  • オートサジェスチョン: Fishシェルのように、入力履歴に基づいて候補を灰色で表示。
  • 複数行編集: 長いコマンドやテキストも快適に編集。
  • マウスサポート: カーソル移動やスクロールにマウスを使用可能(ターミナルが対応している場合)。
  • フルスクリーンアプリケーション: 単純なプロンプトだけでなく、テキストエディタ(Pyvim)やターミナルマルチプレクサ(Pymux)のような複雑なTUIアプリケーションも構築可能。
  • クロスプラットフォーム: Linux, macOS, Windows で動作。
  • Pure Python: C拡張に依存せず、Pythonのみで実装。

このライブラリは、ptpython (高機能Python REPL) や pyvim (Python製Vimクローン) など、多くの人気プロジェクトで使用されています。

💡 ポイント: prompt-toolkit は、単なる入力受付だけでなく、本格的なTUI (Text-based User Interface) アプリケーション開発のフレームワークとしても利用できます。

基本操作: まずはここから始めよう 🔰

インストール

インストールは pip を使うのが最も簡単です。

pip install prompt-toolkit

依存ライブラリとして、シンタックスハイライトのための Pygments と、Unicode文字幅計算のための wcwidth が自動的にインストールされます。

現在の prompt-toolkit のバージョン3系は Python 3.6 以降をサポートしています。(バージョン2系は Python 2.6-3.x もサポートしていましたが、現在は3系が主流です。)

シンプルな入力プロンプト

最も基本的な使い方は prompt() 関数を使うことです。これは Python 標準の input() 関数と似ていますが、より高機能です。

from prompt_toolkit import prompt

if __name__ == '__main__':
    answer = prompt('何か入力してください: ')
    print(f'入力された内容: {answer}')

これを実行すると、指定したメッセージと共にプロンプトが表示され、ユーザーの入力を待ち受けます。入力された文字列が answer 変数に格納されます。この時点でも、Emacsライクなキーバインディング(Ctrl+Aで行頭、Ctrl+Eで行末など)や基本的な編集機能が利用できます。

入力履歴 (History)

複数回の入力を受け付ける場合、以前の入力を再利用できると便利です。prompt() 関数をそのまま複数回呼び出すだけでは履歴は共有されませんが、PromptSession オブジェクトを使うことで簡単に実現できます。

from prompt_toolkit import PromptSession
from prompt_toolkit.history import InMemoryHistory

# セッションを作成 (メモリ上に履歴を保持)
session = PromptSession(history=InMemoryHistory())

while True:
    try:
        text = session.prompt('>>> ')
        print(f'入力: {text}')
        if text.lower() == 'exit':
            break
    except EOFError:
        # Ctrl+D で終了
        break
    except KeyboardInterrupt:
        # Ctrl+C で終了 (任意)
        print("終了します。")
        break

print('セッション終了')

PromptSession を使うと、同じセッション内での prompt() 呼び出し間で入力履歴が共有されます。デフォルトでは InMemoryHistory が使われ、プログラム終了と共に履歴は消えます。

履歴をファイルに保存して永続化したい場合は、FileHistory を使います。

from prompt_toolkit import PromptSession
from prompt_toolkit.history import FileHistory
import os

# ホームディレクトリに履歴ファイルを保存
history_file = os.path.expanduser('~/.my_app_history')
session = PromptSession(history=FileHistory(history_file))

# ... (上記の while ループと同様) ...

これで、アプリケーションを再起動しても前回の履歴が利用できるようになります。⬆️⬇️ キーで履歴を辿ることができます。

高度な機能: prompt-toolkit の真価 ✨

入力補完 (Completion) ⌨️

ユーザーの入力を助ける強力な機能が自動補完です。prompt-toolkit では様々な方法で補完を実装できます。

基本的な単語補完: WordCompleter

最も簡単なのは、あらかじめ定義された単語リストから補完する WordCompleter です。

from prompt_toolkit import PromptSession
from prompt_toolkit.completion import WordCompleter

commands = ['show', 'config', 'interface', 'ip', 'address', 'exit', 'help']
command_completer = WordCompleter(commands, ignore_case=True)

session = PromptSession(completer=command_completer)

while True:
    try:
        text = session.prompt('Router# ')
        print(f'実行: {text}')
        if text.lower() == 'exit':
            break
    except (EOFError, KeyboardInterrupt):
        break

print('終了')

この例では、commands リスト内の単語が補完候補として表示されます (TabキーまたはCtrl+Spaceで表示)。ignore_case=True で大文字小文字を区別せずに補完します。

カスタム補完: Completer クラスの継承

より動的で複雑な補完ロジックを実装するには、Completer 抽象基底クラスを継承します。get_completions メソッドを実装する必要があります。このメソッドは Document オブジェクトと CompleteEvent オブジェクトを受け取り、Completion オブジェクトのイテラブル(通常はリストやジェネレータ)を返します。

from prompt_toolkit import PromptSession
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.document import Document

class MyCustomCompleter(Completer):
    def get_completions(self, document: Document, complete_event):
        text_before_cursor = document.text_before_cursor
        word_before_cursor = document.get_word_before_cursor()

        options = {
            'show': ['version', 'ip', 'interfaces'],
            'config': ['terminal', 'interface'],
            'interface': ['GigabitEthernet0/0', 'Loopback0'],
            'ip': ['address', 'route']
        }

        # 入力中の単語に基づいて候補をフィルタリング
        current_command = text_before_cursor.split()
        current_word = word_before_cursor

        potential_completions = []
        if len(current_command) <= 1 and not text_before_cursor.endswith(' '):
             # 最初のコマンドを補完
             potential_completions = list(options.keys())
        elif len(current_command) >= 1:
            last_full_command = current_command[-2] if text_before_cursor.endswith(' ') else current_command[-1]
            # 最後の完全なコマンドに基づいてサブコマンドを補完
            if last_full_command in options:
                 potential_completions = options[last_full_command]

        for option in potential_completions:
            if option.startswith(current_word):
                yield Completion(
                    option,
                    start_position=-len(current_word),
                    display=f'{option} (カスタム)', # 表示名
                    display_meta='コマンド/サブコマンド' # 補足情報
                )

session = PromptSession(completer=MyCustomCompleter())

# ... (プロンプトループは同様) ...

この例では、入力されたコマンドの文脈に応じて、次のサブコマンド候補を動的に生成しています。Completion オブジェクトでは、補完テキストだけでなく、表示名 (display) や補足情報 (display_meta) もカスタマイズできます。

Fuzzy Completer (あいまい補完)

タイプミスがあってもよしなに補完してくれる「あいまい補完」も利用できます。FuzzyWordCompleter や、より高度な FuzzyCompleter (カスタム `Completer` と組み合わせる) があります。2019年頃には既に実装例が存在していました。

from prompt_toolkit import PromptSession
from prompt_toolkit.completion import FuzzyWordCompleter

animals = ['aardvark', 'antelope', 'baboon', 'badger', 'bison', 'cat', 'dog', 'elephant', 'giraffe']
fuzzy_completer = FuzzyWordCompleter(animals)

session = PromptSession(completer=fuzzy_completer)
text = session.prompt('動物の名前: ')
print(f'選択: {text}')

例えば “elphant” と入力しても “elephant” が候補に出てきます。これは非常に便利です! 👍

入力検証 (Validation) ✅

ユーザーに特定の形式(数値、メールアドレスなど)で入力させたい場合、入力検証機能が役立ちます。入力中にリアルタイムで検証し、不正な場合はエラーメッセージを表示したり、プロンプトの色を変えたりできます。

from prompt_toolkit import PromptSession
from prompt_toolkit.validation import Validator, ValidationError

class NumberValidator(Validator):
    def validate(self, document):
        text = document.text
        if text and not text.isdigit():
            # カーソル位置を見つける
            i = 0
            for i, c in enumerate(text):
                if not c.isdigit():
                    break
            raise ValidationError(message='数字のみ入力可能です', cursor_position=i)

session = PromptSession(validator=NumberValidator(), validate_while_typing=True)

while True:
    try:
        text = session.prompt('数値を入力してください: ', default="abc") # 不正な初期値
        print(f'入力された数値: {text}')
        if text.lower() == 'exit':
             break
    except (EOFError, KeyboardInterrupt):
        break
    except Exception as e:
         print(f"エラー: {e}") # 例えば ValidationError

print('終了')

Validator クラスを継承し、validate メソッドを実装します。不正な入力が見つかった場合、ValidationError を送出します。message でエラー内容を、cursor_position でエラー箇所を指定できます。validate_while_typing=True を指定すると、入力中にリアルタイムで検証が実行されます。

キーバインディング (Key bindings) ⚙️

prompt-toolkit はデフォルトで Emacs 風のキーバインディングを提供しますが、Vi 風に変更したり、独自のキーバインディングを追加したりすることも可能です。

Vi モードへの切り替え

PromptSession または prompt() 関数に vi_mode=True を渡すだけです。

session = PromptSession(vi_mode=True)
# または
# text = prompt('Viモードで入力: ', vi_mode=True)

これで、Esc キーでノーマルモード、i キーで挿入モードといった Vi 特有の操作が可能になります。

カスタムキーバインディング

KeyBindings オブジェクトを使って、特定のキー入力に対してカスタムアクションを割り当てることができます。

from prompt_toolkit import PromptSession
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.keys import Keys

# カスタムキーバインディングを作成
bindings = KeyBindings()

@bindings.add(Keys.ControlT)
def _(event):
    """
    Ctrl+T が押されたら "Hello world!" と挿入する
    """
    event.cli.current_buffer.insert_text('Hello world!')

@bindings.add('c-q') # Control + q でも指定可能
def _(event):
    """
    Ctrl+Q が押されたらアプリケーションを終了する
    """
    event.app.exit() # session.prompt() から抜ける

session = PromptSession(key_bindings=bindings)

try:
    text = session.prompt('カスタムキー束縛 (Ctrl+T, Ctrl+Q): ')
    print(f'入力: {text}')
except (EOFError, KeyboardInterrupt):
    pass # Ctrl+Qで抜けた場合はここには来ないことが多い

print('終了')

@bindings.add() デコレータを使ってキーと関数を結びつけます。キーは Keys Enum や文字列 (‘c-t’, ‘escape’, ‘f1’ など) で指定します。イベントハンドラ関数は event オブジェクトを受け取り、これを通じて現在のアプリケーションの状態(入力バッファ、レイアウトなど)にアクセスしたり、操作したりできます。event.app.exit() はプロンプトを終了させる一般的な方法です。

シンタックスハイライト (Syntax highlighting) 🌈

入力中のテキストを言語の構文に基づいて色付けすることができます。これにより、コードや設定ファイルなどの可読性が大幅に向上します。

Pygments との連携

最も簡単な方法は、強力なシンタックスハイライトライブラリである Pygments を利用することです。PygmentsLexer を使って、特定の Pygments レキサー(言語定義)を適用します。

from prompt_toolkit import PromptSession
from prompt_toolkit.lexers import PygmentsLexer
from pygments.lexers.python import PythonLexer
from pygments.lexers.sql import SqlLexer

# Pythonコード用のセッション
py_session = PromptSession(lexer=PygmentsLexer(PythonLexer))
# SQLクエリ用のセッション
sql_session = PromptSession(lexer=PygmentsLexer(SqlLexer))

try:
    py_code = py_session.prompt('Pythonコードを入力: ')
    print(f'入力されたPythonコード:\n{py_code}')

    sql_query = sql_session.prompt('SQLクエリを入力: ')
    print(f'入力されたSQLクエリ:\n{sql_query}')

except (EOFError, KeyboardInterrupt):
    pass

print('終了')

PygmentsLexer に使用したい Pygments の Lexer クラスを渡すだけで、対応する言語のシンタックスハイライトが有効になります。

カスタムハイライター

特定のニーズに合わせて独自のハイライトロジックを実装したい場合は、Lexer 抽象基底クラスを継承します。

from prompt_toolkit import PromptSession
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.styles import Style

# カスタムスタイル定義
custom_style = Style.from_dict({
    'keyword': '#ff0000 bold', # 赤色、太字
    'comment': 'italic #888888', # 斜体、灰色
    'number': 'fg:ansiblue', # ANSIブルー
})

class SimpleKeywordLexer(Lexer):
    def lex_document(self, document):
        # スタイルとテキストのタプルのリストを返す関数を返す
        def get_line(lineno):
            line = document.lines[lineno]
            result = []
            pos = 0
            for word in line.split(' '):
                 if word in ['SELECT', 'FROM', 'WHERE']:
                     result.append(('class:keyword', word))
                 elif word.isdigit():
                     result.append(('class:number', word))
                 elif word.startswith('#'):
                     result.append(('class:comment', word))
                 else:
                     result.append(('', word)) # デフォルトスタイル
                 result.append(('', ' ')) # スペースを追加
                 pos += len(word) + 1
            return result[:-1] # 最後の余分なスペースを削除

        return get_line

session = PromptSession(lexer=SimpleKeywordLexer(), style=custom_style)

try:
    text = session.prompt('カスタムハイライト: ')
    print(f'入力: {text}')
except (EOFError, KeyboardInterrupt):
    pass

lex_document メソッドは、各行のテキストを受け取り、その行のスタイル情報(スタイルクラス名と対応するテキストのタプルのリスト)を返す関数を返す必要があります。スタイルは Style.from_dict などで定義し、PromptSession に渡します。

ツールバー (Toolbar) 🔧

プロンプトの下部(または上部)に追加情報を表示するためのツールバーを表示できます。現在のモード、入力状態、ヘルプメッセージなどを表示するのに便利です。

import time
from prompt_toolkit import PromptSession
from prompt_toolkit.formatted_text import HTML

def get_bottom_toolbar():
    # 現在の時刻と簡単なヘルプを表示
    current_time = time.strftime("%H:%M:%S")
    return HTML(f'<b>現在時刻:</b> {current_time} | <i>Ctrl+Qで終了</i>')

# 上記のカスタムキーバインディング (Ctrl+Qで終了) も必要
bindings = KeyBindings()
@bindings.add('c-q')
def _(event):
    event.app.exit()

session = PromptSession(bottom_toolbar=get_bottom_toolbar, key_bindings=bindings, refresh_interval=1) # 1秒ごとに更新

try:
    text = session.prompt('ツールバー付きプロンプト: ')
    print(f'入力: {text}')
except (EOFError, KeyboardInterrupt):
    pass

print('終了')

bottom_toolbar (または top_toolbar) 引数に、表示したいテキスト、フォーマット済みテキスト (HTMLなど)、またはそれらを返す関数を指定します。関数を指定すると、refresh_interval で指定した間隔(または入力があるたび)に再評価され、動的な情報を表示できます。

オートサジェスチョン (Auto suggestion) 👉

Fish シェルのように、入力履歴に基づいて入力候補を薄い色で表示する機能です。右矢印キー (→) や Ctrl+E で候補を受け入れることができます。

from prompt_toolkit import PromptSession
from prompt_toolkit.history import InMemoryHistory
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory

# 履歴を共有する必要があるため、Sessionを使う
session = PromptSession(history=InMemoryHistory(),
                          auto_suggest=AutoSuggestFromHistory())

while True:
    try:
        text = session.prompt('サジェスチョン付き: ')
        print(f'入力: {text}')
        if text.lower() == 'exit':
            break
    except (EOFError, KeyboardInterrupt):
        break

print('終了')

auto_suggest 引数に AutoSuggestFromHistory() を渡すだけです。これにより、入力中のテキストが履歴内のエントリの先頭と一致する場合、残りの部分が提案されます。カスタムサジェスチョンロジックを実装することも可能です。

フルスクリーンアプリケーション 🖥️

prompt-toolkit は、単なる1行のプロンプトだけでなく、ターミナル全体を使った複雑なアプリケーション(TUI: Text User Interface)を構築するための機能も備えています。pyvimptpython のようなアプリケーションがこれを利用しています。

フルスクリーンアプリケーションの構築には、以下の主要コンポーネントが登場します。

  • Application: アプリケーション全体を管理するコアオブジェクト。イベントループ、レイアウト、キーバインディングなどを保持します。
  • Layout: 画面の構成要素(ウィンドウ、コンテナ)をどのように配置するかを定義します。垂直分割 (VSplit)、水平分割 (HSplit)、フロート (Float) などを使って複雑なレイアウトを作成できます。
  • Container: 他のコンテナやUIコントロール (Window など) を保持する要素。レイアウトの基本単位です。
  • Window: 実際にテキストやUI要素を表示する領域。BufferControl (テキスト編集用) やカスタムコントロール (UIControl) をコンテンツとして持ちます。
  • BufferControl: テキスト編集機能を提供するコントロール。入力バッファ、カーソル位置、選択範囲などを管理します。
  • KeyBindings: アプリケーション全体、または特定のコントロールにフォーカスがあるときのキー操作を定義します。
  • Style: アプリケーション全体の配色やスタイルを定義します。

簡単なフルスクリーンアプリケーションの例を見てみましょう。

from prompt_toolkit.application import Application
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.layout.containers import HSplit, Window
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.widgets import TextArea

# キーバインディング
bindings = KeyBindings()

@bindings.add('c-q')
def _(event):
    """ Ctrl+Q で終了 """
    event.app.exit()

# レイアウト定義
# 上部に固定テキスト、下部にテキスト入力エリア
body = HSplit([
    Window(content=FormattedTextControl('Hello world! Press Ctrl+Q to quit.'), height=1, style='reverse'),
    Window(height=1, char='-'), # 区切り線
    TextArea(text="ここにテキストを入力できます...")
])

# アプリケーションインスタンス作成
application = Application(
    layout=Layout(body),
    key_bindings=bindings,
    full_screen=True # フルスクリーンモードを有効化
)

# アプリケーション実行
application.run()

print("アプリケーション終了")

この例では、画面上部に固定テキスト、下部に複数行入力可能な TextArea ウィジェットを配置し、Ctrl+Q で終了する単純なフルスクリーンアプリを作成しています。Layout で画面構成を定義し、Application に渡して実行します。full_screen=True がポイントです。

フルスクリーンアプリケーション開発は奥が深く、レイアウトのカスタマイズ、ウィジェットの作成、フォーカス管理など、学ぶべきことは多いですが、非常に表現力豊かなTUIを作成できます。

prompt-toolkit の機能を組み合わせることで、様々な実用的なアプリケーションを構築できます。

  • カスタム REPL (Read-Eval-Print Loop):
    • 特定の言語やフレームワーク専用の対話的シェルを作成できます (例: ptpython)。
    • ドメイン固有言語 (DSL) のインタープリタに、シンタックスハイライトや補完を追加できます。
    • データベースクライアント (例: mycli, pgcli) のように、SQL補完や実行結果表示を備えたツールが作れます。公式ドキュメントには SQLite REPL のチュートリアルもあります。
  • 設定ファイルエディタ:
    • フルスクリーンモードを使って、設定ファイルのキーと値を対話的に編集するツールを作成できます。
    • 入力検証を使って、設定値が正しい形式かチェックできます。
    • 補完機能で、設定可能なキーや値を提示できます。
  • 対話型インストーラー/セットアップツール:
    • ユーザーに必要な情報を質問形式で入力させ、設定を生成するツールを作成できます。
    • プログレスバーを表示して、時間のかかる処理の進捗を示すことができます。
    • ダイアログ (message_dialog, input_dialog, radiolist_dialog など) を使って、より洗練された対話が可能です。(2022年頃の情報でも言及されています)
  • 高機能な CLI ツール:
    • 複雑なコマンド体系を持つツールに、強力な補完機能やヘルプ表示を追加できます。
    • オートサジェスチョンで、よく使うコマンド入力を効率化できます。
    • ツールバーで、ツールの状態やコンテキスト情報を表示できます。
  • その他:
    • テキストベースのアドベンチャーゲーム
    • ターミナルベースのチャットクライアント
    • デバッグツール用の対話インターフェース

可能性は無限大です! 💡

まとめ: prompt-toolkit の魅力とこれから

prompt-toolkit は、Python でリッチなコマンドライン体験を実現するための非常に強力で柔軟なライブラリです。

prompt-toolkit の利点 👍

  • ユーザーエクスペリエンス向上: 補完、ハイライト、履歴などの機能により、CLI アプリが格段に使いやすくなります。
  • 開発効率向上: 面倒なターミナル制御を抽象化し、開発者はアプリケーションのロジックに集中できます。
  • 高いカスタマイズ性: キーバインディング、スタイル、レイアウトなど、細部まで自由にカスタマイズ可能です。
  • Pure Python: 依存関係が少なく、導入が容易です。
  • クロスプラットフォーム: 主要なOSで動作します。
  • フルスクリーン対応: 単純なプロンプトから複雑なTUIまで、幅広いアプリケーションを構築できます。

学習曲線は多少ありますが、基本的な使い方から始めて、必要に応じて高度な機能を学んでいくことができます。公式ドキュメントや豊富なサンプルコードが学習の良い助けとなるでしょう。

CLI は依然として開発やシステム管理において重要なインターフェースであり、prompt-toolkit のようなライブラリを使うことで、その可能性をさらに広げることができます。ぜひ、あなたの次の Python プロジェクトで prompt-toolkit を活用してみてください! 🎉

コメント

タイトルとURLをコピーしました