inquirerpy で対話型インタフェースを作成してみよう

# inquirerpy について
[inquirerpy](https://pypi.org/project/inquirerpy/) は NodeJS での[inquirer.js](https://github.com/SBoudrias/Inquirer.js/) に触発されて開発された、会話形アプリケーションを作成するための拡張モジュールです。
類似のモジュールには [python-inquirer](https://github.com/magmax/python-inquirer) や、[PyInquirer](https://github.com/CITGuru/PyInquirer) があります。
python-inquirer も　[node.js](https://nodejs.org/ja/) の [inquirer.js](https://github.com/SBoudrias/Inquirer.js) に触発されてほぼ同じ実装をPython で実現できるように開発されました。しかし、残念ながら Windows では一部うまく動作しないことが確認されています

これに対して、inquirerpy と PyInquirer は内部的には [prompt_toolkit](https://python-prompt-toolkit.readthedocs.io/en/master/) を使用しているため、Windows でも問題なく動作できます。

ただし、PyInquirer は prompt_toolkit の古いバージョン1.0.14に依存していて、IPython などと共存することができなくなっています。inquirerpy はこの問題を解決するとともに、いくつかのバグを修正するために再開発されています。

inquirerpy は Python 3.7 以降で動作します。

# inquirerpy のインストール
inquirerpy はpipコマンドでインストールできます。

```bash
$ pip install inquirerpy
```

## inquirerpy の互換API
inquirerpy は PyInquirer との互換性を考慮して開発されています。
まずは、PyInquirer との互換APIについて説明することにします。

`prompt()`を使ってユーザから文字列を受け取る簡単な例から見てみましょう。


```python:001_single_input.py
from InquirerPy import prompt

questions = [
    {
        'type': 'input',
        'name': 'first_name',
        'message': "What's your first name",
    }
]

answers = prompt(questions)
print(answers)
print(answers['first_name'])
```

これを実行すると次のような質問が表示されます。

![inquirerpy_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/7792d260-2155-ce9f-0f12-6a12e90e0c6a.png)


`questions` は質問を記述した辞書のリストです。
`prompt()` は質問の定義に従ってユーザからの入力を受け取り、辞書オブジェクトとして結果を返します。

リストにある複数の質問は続けて処理されます。

```python:002_multiple_input.py
from InquirerPy import prompt

questions = [
    {
        'type': 'input',
        'name': 'first_name',
        'message': "What's your first name",
    },
    {
        'type': 'input',
        'name': 'last_name',
        'message': "What's your last name",
        'when': lambda answers: answers['first_name'] != '',
    }
]

answers = prompt(questions)
print(answers)
```

```
$ python 002_multiple_input.py
? What's your first name Jack
? What's your last name Bauer
{'first_name': 'Jack', 'last_name': 'Bauer'}
```



質問の辞書に、`when`で条件が与えられていると、その条件に合致するときだけ、その質問はユーザに問い合わせが行われます。



## 入力補完
ユーザから文字列を受け取るときに、予め登録した文字列に部分合致するか、はじめにタブキーを押下すると補完文字列をポップアップ表示します。このとき文字列入力中に
タブキーを押下すると残りの入力を補完したデータを返します。
ポップアップされた候補をカーソルキーで選ぶこともできます。

```python:003_completion.py
from InquirerPy import prompt

questions = [
   {
       "type": "input",
       "message": "Which company would you like to apply:",
       "completer": {
           "Google": None,
           "Facebook": None,
           "Amazon": None,
           "Netflix": None,
           "Apple": None,
           "Microsoft": None,
       },
       "multicolumn_complete": True,
   },
]

answers = prompt(questions)
print(answers)
```


![inquirerpy_003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/23c2fe16-067a-b292-cb6d-3d9639f52e3f.png)


## transformer, filter, validate
ユーザの入力を加工したり、結果を変換したり、データ検証を行わせることができます。


```python:004_validation.py
from InquirerPy import prompt
from InquirerPy.validator import NumberValidator

questions = [
   {
       "name": "request salary",
       "type": "input",
       "message": "What's your salary expectation(k):",
       "transformer": lambda result: "%sk" % result,
       "filter": lambda result: int(result) * 1000,
       "validate": NumberValidator(),
   },
]

answer = prompt(questions)
print(answer)
```

`transformer`は入力された文字列を加工して画面に出力します。`filter`は入力されたデータを加工して返します。

![inquirerpy_004_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/f54c374e-2715-6fd8-2a38-a92c4fceb029.png)
![inquirerpy_004_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/568558c2-a390-68d9-a141-350e40449f10.png)

`validate`で与えた検証が失敗すると入力は完了せずに、エラーメッセージを出力して再度入力待ちになります。

![inquirerpy_004_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/f28178dc-4987-46c3-0d23-896980cc6cc8.png)




## 質問のタイプ
質問を定義する辞書に与えるキー`type`で、質問のタイプを指定します。
次のようなタイプがあります。

 *  `list`: リストした候補をカーソルキーで選択する
 * `rawlist`：数値付きでリストした候補を数値で選択する
 * `expand`：リストのコンパクト表示、キーアクションで展開表示
 * `checkbox`：チョイスボックス
 * `confirm`：ユーザに Yes/No の確認をしブール値を返す
 * `input`：プロンプトを表示してユーザから文字列を受け取る
 * `password`：`input`と似ているがユーザの入力は表示されない
 * `fazzy`: 
 * `editor`：ユーザから複数行の入力を受け取る

## list タイプ
選択肢を表示してユーザに選ばせる基本的なUIです。

プロパティー:`type`, `name`, `message`, `choices`, `default`、
`[filter]`、`[transformer]`、`[validate]`、`[multiselect]`、`[invalid_message]`

`filter`で与えた処理がユーザの入力に適用されて返されます。
`transformer`は返り値には影響は与えませんが、表示文字列を変更します。
`multiselect`が`True`のときは複数の選択を許します。

```python:005_list_simple.py
from InquirerPy import prompt

questions = [
    {
        "type": "list",
        "message": "Select an action:",
        "choices": ["Upload", "Download",
                    {"name": "Exit", "value": None}],
        "default": None,
        # "multiselect": True,
    },
]

answer = prompt(questions=questions)
print(answer)
```

カーソルキーで候補を選択します。

![inquirerpy_005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/71d5fe92-263c-7333-6038-ff4c0a0c5adb.png)


`multiselect`に`True`を与えると、複数選択することができラジオボタンのように動作します。タブキー、スペースキーで候補を選択できます。


## Separator
`Separator`クラスのインスタンスオブジェクトを与えると、一覧するリストに区切り文字を表示するゆになります。


```python:006_list_separator.py
from InquirerPy import prompt
from InquirerPy.separator import Separator

questions = [
    {
        "name": "region",
        "type": "list",
        "message": "Select regions:",
        "choices": [
            {"name": "Sydney", "value": "ap-southeast-2"},
            {"name": "Singapore", "value": "ap-southeast-1"},
            Separator(),
            "us-east-1",
            Separator(line='*' * 15),
            "us-east-2",
        ],
        "multiselect": True,
        "transformer": lambda result: "%s region%s selected"
        % (len(result), "s" if len(result) > 1 else ""),
    },
]

result = prompt(questions=questions)
print(result)
```

![inquirerpy_006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/7d27e3bb-df5c-ed4b-98ed-9d730132d934.png)



## rawlist タイプ
自動的に数字のショートカットを適用させたリストプロンプトです。
各選択肢の前には数字が付加されます。この数字をショートカットキーとして使用して選択肢にジャンプしたり、通常の上下キーバインドで移動したりすることができます。

プロパティー:`type`, `name`, `message`, `choices`, `default`、
`[filter]`、`[transformer]`、`[validate]`、`[multiselect]`、`[invalid_message]`


```python:007_rawlist.py
from InquirerPy import prompt

questions = [
    {
        "type": "rawlist",
        "choices": [
            "Apple", "Orange", "Peach", "Cherry",
            "Melon", "Strawberry", "Grapes",
        ],
        "message": "Pick your favourites:",
        "default": 3,
        "multiselect": True,
        "transformer": lambda result: ", ".join(result),
        "validate": lambda result: len(result) > 1,
        "invalid_message": "Minimum 2 selections",
    },
]

fruits = prompt(questions)
print(fruits)
```

![inquirerpy_007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/f1d73892-56d3-aca2-9734-aec352b15853.png)


## expand タイプ
コンパクト表示(compact)と拡大表示(expand)の2種類のUIを持つリストプロンプトです。
はじめはコンパクトな状態で表示されますが、`h`キーを使って拡大することができます。拡大後は、通常のリストナビゲーションキーが有効になります。

プロパティー:`type`, `name`, `message`, `choices`, `default`、
`[filter]`、`[transformer]`、`[validate]`、`[multiselect]`、`[invalid_message]`、`[instruction]`


```python:008_expand.py
from InquirerPy import prompt

fruits_choice = [
        {"key": "a", "name": "Apple", "value": "Apple"},
        {"key": "c", "name": "Cherry", "value": "Cherry"},
        {"key": "o", "name": "Orange", "value": "Orange"},
        {"key": "p", "name": "Peach", "value": "Peach"},
        {"key": "m", "name": "Melon", "value": "Melon"},
        {"key": "s", "name": "Strawberry", "value": "Strawberry"},
        {"key": "g", "name": "Grapes", "value": "Grapes"},
]

questions = [
    {
        "type": "expand",
        "choices": fruits_choice,
        "message": "Pick your favourite:",
        "default": "o",
        "cycle": False,
    },
]

result = prompt(questions)
print(result)
```

![inquirerpy_008_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/126122d4-c2c6-b359-a97f-31a9e252f7ee.png)

![inquirerpy_008_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/d80908be-86fd-b732-8c09-7bc63e908ae5.png)



## checkbox タイプ
チェックボックスを表示するリストプロンプト。

マルチセレクトが可能な他のリストプロンプトでは、最小選択数は常に1です（ユーザが何も選択しない場合、現在のハイライトされた選択肢が選択されます）。チェックボックスプロンプトでは、現在のハイライトされた選択肢は自動的に選択されません。

プロパティー:`type`, `name`, `message`, `choices`, `default`、
`[filter]`、`[transformer]`、`[validate]`、`[multiselect]`、`[invalid_message]`、`[instruction]`

`choices` で与える各選択肢は、文字列表現を持つ任意の値、または以下のキーで構成される辞書にすることができます。

 * `name`:選択肢の表示名で、ユーザーはこの値を見ることができます。
 * `value`選択肢の値。名前とは異なる場合がありますが、ユーザーはこの値を見ることはできません。
 * `enabled`: 選択肢が選択された状態にあるかどうかをTrue/Falseで与える

`choices` で `{'enabled'：True}`とされた選択肢は、デフォルトでチェックされます。


```python:009_checkbox.py
from InquirerPy import prompt
from InquirerPy.separator import Separator

region_choice = [
    Separator(),
    {"name": "Sydney", "value": "ap-southeast-2", "enabled": False},
    {"name": "Singapore", "value": "ap-southeast-1", "enabled": True},
    Separator(),
    "us-east-1",
    "us-west-1",
    Separator(),
]

questions = [
    {
        "type": "checkbox",
        "message": "Select regions:",
        "choices": region_choice,
        "transformer": lambda result: "%s region%s selected"
        % (len(result), "s" if len(result) > 1 else ""),
    },
]

result = prompt(questions)
print(result)
```

![inquirerpy_009.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/9824ff46-7d19-bd44-29ac-1bd82947867c.png)



## confirm タイプ
ユーザーの確認を取るためのプロンプトです。

プロパティー:`type`, `name`, `message`, `choices`,`[default]`, `[when]`
`default` はブール値である必要があります。

```python:010_confirm.py
from InquirerPy import prompt

questions = [
    {
        "type": "confirm",
        "message": "Proceed?",
        "name": "proceed",
        "default": True,
    },
    {
        "type": "confirm",
        "message": "Require 1 on 1?",
        "when": lambda result: result["proceed"],
    },
    {
        "type": "confirm",
        "message": "Confirm?",
        "when": lambda result: result.get(1, False),
    },
]

result = prompt(questions)
print(result)
```

![inquirerpy_010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/ca6f8351-a2f3-893d-c9a5-367830b30826.png)


この例のように連続した質問では、`when` で指定した質問の応答がTrueとなるときだけ質問するようにもできます。

## input タイプ
ユーザーにいくつかのテキスト値を入力させる入力プロンプトです。`Ctrl-Space`もしくは TABキーを押下すると、`completter` が与えられている場合は、自動コンプリートのポップアップが強制的に表示されます。

プロパティー:`type`, `name`, `message`,, `default`、
`[filter]`、`[transformer]`、`[validate]`、`[multiline]`、`[invalid_message]`、

 * `message` で与えた文字列をプロンプトとして表示し、ユーザから文字列の入力を受け付けます。
 * `default` にはデフォルト値を与えることができます。
 * `completer` には補完文字列を登録することができます。
 * `filter`で与えた処理がユーザの入力に適用されて返されます。
 * `validate` で与えた関数で入力値を検証することができます。
 * `multiline`が `True`のときは複数行の入力を許します。

```python:011_input.py
from prompt_toolkit.validation import Validator, ValidationError
from pprint import pprint
from InquirerPy import prompt

class AgeValidator(Validator):
    def validate(self, document):
        if int(document.text) < 18:
            raise ValidationError(
                message='Too yound.',
                cursor_position=len(document.text))  # Move cursor to end

questions = [
  {
    'type': 'input',
    'name': 'first_name',
    'message': "What's your first name ?",
  },
  {
    'type': 'input',
    'name': 'last_name',
    'message': "What's your last name ?",
    'default': lambda ans: 'Bauer' if ans['first_name'] == 'Jack' else '',
  },
  {
    'type': 'input',
    'name': 'age',
    'message': "How old are you ?",
    'validate': AgeValidator()
  }
]
result = prompt(questions)
pprint(result)
```

![inquirerpy_011.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/fa6488c0-1d93-09de-0503-d09b019ce8e8.png)

first name に Jack と入力されると、last name のデフォルトは Bauer になります。
age では `AgeValidator()`で入力値が処理され異常値であればエラーが表示されて再度入力が求められる状態になります。



## passwrd タイプ
ユーザーにパスワードなどの秘密の値を入力させるための入力プロンプトです。inputタイプと似ていますがユーザが入力した文字列は画面には表示されません。



プロパティー:`type`, `name`, `message`,  `[default]`, `[filter]`, `[validate]`

 * `message` で与えた文字列をプロンプトとして表示し、ユーザから文字列の入力を受け付けます。
 * `default` にはデフォルト値を与えることができます。
 * `filter`で与えた処理がユーザの入力に適用されて返されます。
 * `validate` で与えた関数で入力値を検証することができます。


```python:012_password.py
from InquirerPy import prompt
from InquirerPy.validator import PasswordValidator

original_password = "P@ssw0rd123"

questions = [
    {
        "type": "password",
        "message": "Old password:",
        "transformer": lambda _: "[hidden]",
        "validate": lambda text: text == original_password,
        "invalid_message": "Wrong password",
    },
    {
        "type": "password",
        "message": "New password:",
        "name": "new_password",
        "validate": PasswordValidator(
            length=8, cap=True, special=True, number=True
        ),
        "transformer": lambda _: "[hidden]",
    },
    {"type": "confirm", "message": "Confirm?", "default": True},
]

result = prompt(questions)
print(result)
```

![inquirerpy_012_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/91a05dde-0784-412c-bd01-3171edf9915f.png)
![inquirerpy_012_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/57ba1542-2fe2-794e-fafd-57fc74535618.png)


## filepath タイプ
ファイルパスの補完機能があらかじめ組み込まれた入力プロンプト。

```python:013_filepath.py
from pathlib import Path
from InquirerPy import prompt
from InquirerPy.validator import PathValidator

questions = [
    {
        "type": "filepath",
        "message": "Enter file to upload:",
        "name": "location",
        "default": str(Path('/tmp')),
        "validate": PathValidator(is_file=True, message="Input is not a file"),
        "only_files": True,
    },
    {
        "type": "filepath",
        "message": "Enter path to download:",
        "validate": PathValidator(is_dir=True, message="Input is not a directory"),
        "name": "destination",
        "only_directories": True,
    },
]

result = prompt(questions)
print(result)
```


![inquirerpy_013.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/021c472b-3f40-1766-f1d5-b0b7ea6b3358.png)


## Fuzzy タイプ
入寮文字列のあいまい検索機能を持つリストプロンプトです。選択肢の数が約30万に達するまでは比較的良好な性能を発揮します。

```python:014_fuzzy.py
from contextlib import ExitStack
from pathlib import Path
from InquirerPy import inquirer, prompt

_WORDFILE_ = 'sample.txt'
def get_choices(_):
    p = Path.cwd().joinpath(_WORDFILE_)
    choices = []

    with ExitStack() as stack:
        if not p.exists():
            raise Exception('%s: wordfile missing!', _WORDFILE_)
        else:
            file = stack.enter_context(p.open("r"))
        for line in file.readlines():
            choices.append(line[:-1])
    return choices

questions = [
    {
        "type": "fuzzy",
        "message": "Select actions:",
        "choices": ["hello", "weather", "what", "whoa", "hey", "yo"],
        "default": "he",
        "max_height": "70%",
    },
    {
        "type": "fuzzy",
        "message": "Select preferred words:",
        "choices": get_choices,
        "multiselect": True,
        "validate": lambda result: len(result) > 1,
        "invalid_message": "minimum 2 selection",
        "max_height": "70%",
    },
]

result = prompt(questions=questions)
print(result)
```

![inquirerpy_014.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/182364/48e5a254-de38-3769-f6bd-c27645fc26ef.png)



## キーバインド
`keybindings`でキーバインドを定義することができます。
デフォルトのキーバインドは次のように定義されています。

```python{
    "down": [
        {"key": "down"},
        {"key": "c-n", "filter": ~self._is_vim_edit},
        {"key": "j", "filter": self._is_vim_edit},
    ],
    "up": [
        {"key": "up"},
        {"key": "c-p", "filter": ~self._is_vim_edit},
        {"key": "k", "filter": self._is_vim_edit},
    ],
    "toggle": [
        {"key": "space"},
    ],
    "toggle-down": [
        {"key": Keys.Tab},
    ],
    "toggle-up": [
        {"key": Keys.BackTab},
    ],
    "toggle-all": [
        {"key": "alt-r"},
    ],
    "toggle-all-true": [
        {"key": "alt-a"},
    ],
    "toggle-all-false": [],
}
```


`Ctrl+n`でカーソルキーの下キー、`Ctrl+p`でカーソルキーの上キーと同じ動作になります。
`prompt()`の引数に`vi_mode=True`を与えるとキーバインドが vi コマンドと同様に、`j`でカーソルキーの下キー、`k`でカーソルキーの上キーと同じ動作になります。
キーバインドを変更したい場合は、`prompt()`にキーワード引数　`keybinding=` に辞書を与えます。
`raise_keyboard_interrupt=False`を与えると、キーボードからの `Ctrl-C`割り込みを無視するようになります。



#### キーバインドの表記方法
 
| キー               |  表記                                       |
|:-------------------|:--------------------------------------------|
| Escape             | escape                                      |
| Arrows             | left, right, up, down                       |
| Navigation         | home, end, delete, pageup, pagedown, insert |
| Control+lowercase  | c-a, c-b ... c-y, c-z                       |
| Control+uppercase  | c-A, c-B ... c-Y, c-Z                       |
| Control + arrow    | c-left, c-right, c-up, c-down               |
| Other control keys | c-@, c-\, c-], c-^, c-\_, c-delete          |
| Shift + arrow      | s-left, s-right, s-up, s-down               |
| Other shift keys   | s-delete, s-tab                             |
| Function-keys      | f1, f2, .... f23, f24                       |
| Alt+lowercase      | alt-a, alt-b ... alt-y, alt-z               |
| Alt+uppercase      | alt-A, alt-B ... alt-Y, alt-Z               |
| Control+Space      | c-@                                         |
| Enter              | c-m                                         |
| Tab                | c-i                                         |



## スタイル
スタイルをカスタマイズする場合、辞書でスタイルを定義して、`style_from_dict()` に渡したオブジェクトを`prompt()`の`style`キーワードに与えます。

```python:custom_style.py
from InquirerPy import prompt, style_from_dict

custom_style_1 = style_from_dict({
    "separator": '#cc5454',
    "questionmark": '#673ab7 bold',
    "selected": '#cc5454',  # default
    "pointer": '#673ab7 bold',
    "instruction": '',  # default
   "answer": '#f44336 bold',
    "question": '',
})
# ... （中略)
answers = prompt(questions, style=custom_style_1)
```

## 新しいAPI

inquirerpy では、オリジナルの PyInquirer とは異なる、よりシンプルなAPIを提供しています。
これまで例示したサンプルコードのファイル名と合わせた連番に100を加えたファイル名にしています。

 * 互換API：`001_singple_input.py`
 * 新しいAPI：`101_single_input.py`

PyInquirer との互換APIでは、辞書で定義した質問を`prompt()`に合わえていました。
新しいAPIでは、`Inquirer()`でマップされる各UIクラスを使用します。

`site-packages/InquirePy/resolver.py `からの抜粋

```python
from InquirerPy.prompts.checkbox import CheckboxPrompt
from InquirerPy.prompts.confirm import ConfirmPrompt
from InquirerPy.prompts.expand import ExpandPrompt
from InquirerPy.prompts.filepath import FilePathPrompt
from InquirerPy.prompts.fuzzy import FuzzyPrompt
from InquirerPy.prompts.input import InputPrompt
from InquirerPy.prompts.list import ListPrompt
from InquirerPy.prompts.rawlist import RawlistPrompt
from InquirerPy.prompts.secret import SecretPrompt

uestion_mapping = {
    "confirm": ConfirmPrompt,
    "filepath": FilePathPrompt,
    "password": SecretPrompt,
    "input": InputPrompt,
    "list": ListPrompt,
    "checkbox": CheckboxPrompt,
    "rawlist": RawlistPrompt,
    "expand": ExpandPrompt,
    "fuzzy": FuzzyPrompt,
}

list_prompts = {"list", "checkbox", "rawlist", "expand", "fuzzy"}
```

####　互換APIのタイプと新しいAPIのメソッドの比較

| 互換API  | 新しいAPI   | UIの説明
|:---------|:------------|:------------------------------------------|
| input    | text()      | ユーザから文字列を受け取る    |
| list     | select()    | 選択肢を表示してユーザに選ばせる    |
| rawlist  | rawlist()   | 選択肢を番号付きで表示してユーザに番号を選ばせる    |
| expand   | expand()    | コンパクト表示した後、キーで展開して選ばせる    |
| checkbox | ncheckbox() | 選択肢をチェックボックスで表示し、ユーザの選択をリス>トで返す    |
| confirm  | nconfirm()  | ユーザにYES/NOの確認を行う    |
| password | nsecret()   | input/text() と同じですが、ユーザの入力文字列は表示されない    |
| filepath | nfilepath() | ファイルパスの補完機能があらかじめ組み込まれたUI    |
| fuzzy    | nfuzzy()    | ユーザの入力を元にあいまい検索の機能を持つUI    |


新しいAPIのクラスをエイリアスで定義しています。

`site-packages/Inquirerpy/inquirer.py` から抜粋

```python
from InquirerPy.prompts import CheckboxPrompt as checkbox
from InquirerPy.prompts import ConfirmPrompt as confirm
from InquirerPy.prompts import ExpandPrompt as expand
from InquirerPy.prompts import FilePathPrompt as filepath
from InquirerPy.prompts import FuzzyPrompt as fuzzy
from InquirerPy.prompts import InputPrompt as text
from InquirerPy.prompts import ListPrompt as select
from InquirerPy.prompts import RawlistPrompt as rawlist
from InquirerPy.prompts import SecretPrompt as secret
```



ユーザから文字列を受け取る簡単な例から見てみましょう。
同じ結果になるので実行例は省略しています。


```python:101_single_input.py
from InquirerPy import inquirer

name = inquirer.text(message="What's your name: ").execute()
print(name)
```

このケースでは、互換APIでは、`prompt()`に与える辞書の質問のタイプとして `input`を使用しますが、新しいAPIでは、
`text()`メソッドを使用します。タイプの名前とは異なるので注意してください。

連続する質問は、互換APIでは質問の辞書をリストで与えますが、新しいAPIでは質問は独立しているため、
都度`Inquirer`クラスのメソッドを呼び出します。


```python:102_multiple_input.py
from InquirerPy import inquirer

first_name = inquirer.text(
                      message="What's your first name: ").execute()
if first_name != '':
    last_name = inquirer.text(
                          message="What's your last name: ").execute()
else:
    last_name = ''

print(first_name, last_name)
```


## 入力補完
ユーザから文字列を受け取るときに、予め登録した文字列に部分合致すると補完文字列をポップアップ表示します。
タブキーを押下すると残りの入力を補完したデータを返します。


```python:103_completion.py
from InquirerPy import inquirer

answer = inquirer.text(
        message="Which company would you like to apply:",
        completer={
            "Google": None,
            "Facebook": None,
            "Amazon": None,
            "Netflix": None,
            "Apple": None,
            "Microsoft": None,
        },
        multicolumn_complete=True,
    ).execute()

print(answer)
```


## transformer, filter, validate
ユーザの入力を加工したり、結果を変換したり、データ検証を行わせることができます。

```python:104_validation.py
from InquirerPy import inquirer
from InquirerPy.validator import NumberValidator

answer = inquirer.text(
        message="What's your salary expectation(k):",
        transformer=lambda result: "%sk" % result,
        filter=lambda result: int(result) * 1000,
        validate=NumberValidator(),
).execute()

print(answer)
```




## ListPrompt()
選択肢を表示してユーザに選ばせる基本的なUIです。


```python
class ListPrompt(BaseListPrompt):
    def __init__(
        self,
        message: Union[str, Callable[[SessionResult], str]],
        choices: Union[Callable[[SessionResult], List[Any]], List[Any]],
        default: Any = None,
        style: InquirerPyStyle = None,
        vi_mode: bool = False,
        qmark: str = "?",
        pointer: str = INQUIRERPY_POINTER_SEQUENCE,
        instruction: str = "",
        transformer: Callable[[Any], Any] = None,
        filter: Callable[[Any], Any] = None,
        height: Union[int, str] = None,
        max_height: Union[int, str] = None,
        multiselect: bool = False,
        marker: str = INQUIRERPY_POINTER_SEQUENCE,
        validate: Union[Callable[[Any], bool], Validator] = None,
        invalid_message: str = "Invalid input",
        keybindings: Dict[str, List[Dict[str, Any]]] = None,
        show_cursor: bool = True,
    ) -> None:
```


```python:105_list_simple.py
from InquirerPy import inquirer
from InquirerPy.separator import Separator

action = inquirer.select(
        message="Select an action:",
        choices=["Upload", "Download", {"name": "Exit", "value": None}],
        default=None,
        # multiselect=True,
).execute()

print(action)
```


## Separator
`Separator`クラスのインスタンスオブジェクトを与えると、一覧するリストに区切り文字を表示するようになります。


```python:106_list_separator.py
from InquirerPy import inquirer
from InquirerPy.separator import Separator

region = inquirer.select(
            message="Select regions:",
            choices=[
                {"name": "Sydney", "value": "ap-southeast-2"},
                {"name": "Singapore", "value": "ap-southeast-1"},
                Separator(),
                "us-east-1",
                Separator(line='*' * 15),
                "us-east-2",
            ],
            multiselect=True,
            transformer=lambda result: "%s region%s selected"
            % (len(result), "s" if len(result) > 1 else ""),
        ).execute()

print(region)
```




## RawlistPrompt()
自動的に数字のショートカットを適用させたリストプロンプトです。
各選択肢の前には数字が付加されます。この数字をショートカットキーとして使用して選択肢にジャンプしたり、通常の上下キーバインドで移動したりすることができます。


```python
class RawlistPrompt(BaseListPrompt):
    def __init__(
        self,
        message: Union[str, Callable[[SessionResult], str]],
        choices: Union[Callable[[SessionResult], List[Any]], List[Any]],
        default: Any = None,
        separator: str = ")",
        style: InquirerPyStyle = None,
        vi_mode: bool = False,
        qmark: str = "?",
        pointer: str = " ",
        instruction: str = "",
        transformer: Callable[[Any], Any] = None,
        filter: Callable[[Any], Any] = None,
        height: Union[int, str] = None,
        max_height: Union[int, str] = None,
        multiselect: bool = False,
        marker: str = INQUIRERPY_POINTER_SEQUENCE,
        validate: Union[Callable[[Any], bool], Validator] = None,
        invalid_message: str = "Invalid input",
        keybindings: Dict[str, List[Dict[str, Any]]] = None,
        show_cursor: bool = True,
    ) -> None:
```

```python:107_rawlist.py
from InquirerPy import inquirer

fruit = inquirer.rawlist(
    message="Pick your favourites:",
    choices=[
        "Apple",
        "Orange",
        "Peach",
        "Cherry",
        "Melon",
        "Strawberry",
        "Grapes",
    ],
    default=3,
    multiselect=True,
    transformer=lambda result: ", ".join(result),
    validate=lambda result: len(result) > 1,
    invalid_message="Minimum 2 selections",
).execute()

print(fruit)
```



## ExpandPrompt()
コンパクト(compact)と拡張(expand)の2種類のUIを持つリストプロンプトです。
はじめはコンパクトな状態で表示されますが、`h`キーを使って拡張することができます。展開後は、通常のリストナビゲーションキーが有効になります。


```python
class ExpandPrompt(BaseListPrompt):
    def __init__(
        self,
        message: Union[str, Callable[[SessionResult], str]],
        choices: Union[Callable[[SessionResult], List[Any]], List[Any]],
        default: Any = "",
        style: InquirerPyStyle = None,
        vi_mode: bool = False,
        qmark: str = "?",
        pointer: str = " ",
        separator: str = ")",
        help_msg: str = "Help, list all choices",
        expand_pointer: str = INQUIRERPY_POINTER_SEQUENCE,
        instruction: str = "",
        transformer: Callable[[Any], Any] = None,
        filter: Callable[[Any], Any] = None,
        height: Union[int, str] = None,
        max_height: Union[int, str] = None,
        multiselect: bool = False,
        marker: str = INQUIRERPY_POINTER_SEQUENCE,
        validate: Union[Callable[[Any], bool], Validator] = None,
        invalid_message: str = "Invalid input",
        keybindings: Dict[str, List[Dict[str, Any]]] = None,
        show_cursor: bool = True,
    ) -> None:
```


```python:108_expand.py
from InquirerPy import inquirer

fruits_choice = [
        {"key": "a", "name": "Apple", "value": "Apple"},
        {"key": "c", "name": "Cherry", "value": "Cherry"},
        {"key": "o", "name": "Orange", "value": "Orange"},
        {"key": "p", "name": "Peach", "value": "Peach"},
        {"key": "m", "name": "Melon", "value": "Melon"},
        {"key": "s", "name": "Strawberry", "value": "Strawberry"},
        {"key": "g", "name": "Grapes", "value": "Grapes"},
    ]

fruit = inquirer.expand(
        message="Pick your favourite:",
        choices=fruits_choice,
        default="o"
    ).execute()

print(fruit)
```



## CheckboxPrompt()
チェックボックスを表示するリストプロンプト。

マルチセレクトが可能な他のリストプロンプトでは、最小選択数は常に1です（ユーザが何も選択しない場合、現在のハイライトされた選択肢が選択されます）。チェックボックスプロンプトでは、現在のハイライトされた選択肢は自動的に選択されません。

```python
class CheckboxPrompt(BaseListPrompt):
    def __init__(
        self,
        message: Union[str, Callable[[SessionResult], str]],
        choices: Union[Callable[[SessionResult], List[Any]], List[Any]],
        default: Any = None,
        style: InquirerPyStyle = None,
        vi_mode: bool = False,
        qmark: str = "?",
        pointer: str = INQUIRERPY_POINTER_SEQUENCE,
        enabled_symbol: str = INQUIRERPY_FILL_HEX_SEQUENCE,
        disabled_symbol: str = INQUIRERPY_EMPTY_HEX_SEQUENCE,
        instruction: str = "",
        transformer: Callable[[Any], Any] = None,
        filter: Callable[[Any], Any] = None,
        height: Union[int, str] = None,
        max_height: Union[int, str] = None,
        validate: Union[Callable[[Any], bool], Validator] = None,
        invalid_message: str = "Invalid input",
        keybindings: Dict[str, List[Dict[str, Any]]] = None,
        show_cursor: bool = True,
    ) -> None:
```


```python:109_checkbox.py
from InquirerPy import inquirer
from InquirerPy.separator import Separator

region_choice = [
    Separator(),
    {"name": "Sydney", "value": "ap-southeast-2", "enabled": False},
    {"name": "Singapore", "value": "ap-southeast-1", "enabled": True},
    Separator(),
    "us-east-1",
    "us-west-1",
    Separator(),
]

regions = inquirer.checkbox(
    message="Select regions:",
    choices=region_choice,
    cycle=False,
    transformer=lambda result: "%s region%s selected"
    % (len(result), "s" if len(result) > 1 else ""),
).execute()
print(regions)
```



## ConfirmPrompt()
ユーザーの確認を取るためのプロンプトです。


```python
class ConfirmPrompt(BaseSimplePrompt):
    def __init__(
        self,
        message: Union[str, Callable[[SessionResult], str]],
        style: InquirerPyStyle = None,
        default: Union[bool, Callable[[Dict[str, Any]], bool]] = False,
        qmark: str = "?",
        transformer: Callable[[bool], Any] = None,
        filter: Callable[[bool], Any] = None,
        **kwargs
    ) -> None:
```

```python:110_confirm.py
from InquirerPy import inquirer

proceed, service, confirm = False, False, False
proceed = inquirer.confirm(message="Proceed?", default=True).execute()
if proceed:
    service = inquirer.confirm(message="Require 1 on 1?").execute()
if service:
    confirm = inquirer.confirm(message="Confirm?").execute()

print(f'proceed:{proceed}, service:{service}, confirm:{confirm}')
```



## InputPromot()
ユーザーにいくつかのテキスト値を入力させる入力プロンプトです。`Ctrl-Space`もしくは TABキーを押下すると、`completter` が与えられている場合は、自動コンプリートのポップアップが強制的に表示されます。

```python
class InputPrompt(BaseSimplePrompt):
    def __init__(
        self,
        message: Union[str, Callable[[SessionResult], str]],
        style: InquirerPyStyle = None,
        vi_mode: bool = False,
        default: Union[str, Callable[[SessionResult], str]] = "",
        qmark: str = "?",
        completer: Union[Dict[str, Optional[str]], Completer] = None,
        multicolumn_complete: bool = False,
        multiline: bool = False,
        validate: Union[Callable[[str], bool], Validator] = None,
        invalid_message: str = "Invalid input",
        transformer: Callable[[str], Any] = None,
        filter: Callable[[str], Any] = None,
        **kwargs,
    ) -> None:

```


```python:111_input.py
from prompt_toolkit.validation import Validator, ValidationError
from InquirerPy import inquirer

class AgeValidator(Validator):
    def validate(self, document):
        if int(document.text) < 18:
            raise ValidationError(
                message='Too yound.',
                cursor_position=len(document.text))  # Move cursor to end

first_name = inquirer.text(message="Waht's your first name?").execute()

default_name = 'Bauer' if first_name == 'Jack' else ''
last_name = inquirer.text(message="What's your last name?",
                        default=default_name).execute()
age = inquirer.text(message="How old are you?",
                        validate=AgeValidator()).execute()


print(f'first_name: {first_name}, last_name: {last_name}, age: {age}')
```



## SecretPrompt()
ユーザーにパスワードなどの秘密の値を入力させるための入力プロンプトです。inputタイプと似ていますがユーザが入力した文字列は画面には表示されません。

```python
class SecretPrompt(InputPrompt):
    def __init__(
        self,
        message: Union[str, Callable[[SessionResult], str]],
        style: InquirerPyStyle = None,
        default: Union[str, Callable[[SessionResult], str]] = "",
        qmark: str = "?",
        vi_mode: bool = False,
        validate: Union[Validator, Callable[[str], bool]] = None,
        invalid_message: str = "Invalid input",
        transformer: Callable[[str], Any] = None,
        filter: Callable[[str], Any] = None,
        **kwargs
    ) -> None:
```


```python:112_password.py
from InquirerPy import inquirer
from InquirerPy.validator import PasswordValidator

original_password = "P@ssw0rd123"

old_password = inquirer.secret(
    message="Old password:",
    transformer=lambda _: "[hidden]",
    validate=lambda text: text == original_password,
    invalid_message="Wrong password",
).execute()

new_password = inquirer.secret(
    message="New password:",
    validate=PasswordValidator(length=8, cap=True, special=True, number=True),
    transformer=lambda _: "[hidden]",
).execute()

confirm = inquirer.confirm(message="Confirm?", default=True).execute()
msg = f'old_password: {old_password}, '
msg += f'new_password: {new_password}, '
msg += f'confirm: {confirm}'
print(f'{msg}')
```

## FilePathPrompt()


```python
class FilePathPrompt(InputPrompt)"
    def __init__(
        self,
        message: Union[str, Callable[[SessionResult], str]],
        style: InquirerPyStyle = None,
        vi_mode: bool = False,
        default: Union[str, Callable[[SessionResult], str]] = "",
        qmark: str = "?",
        amark: str = "?",
        instruction: str = "",
        multicolumn_complete: bool = False,
        validate: Union[Callable[[str], bool], Validator] = None,
        invalid_message: str = "Invalid input",
        only_directories: bool = False,
        only_files: bool = False,
        transformer: Callable[[str], Any] = None,
        filter: Callable[[str], Any] = None,
        wrap_lines: bool = True,
        session_result: SessionResult = None,
        **kwargs,
    ) -> None:
```

```python:113_filepath.py
from pathlib import Path
from InquirerPy import inquirer
from InquirerPy.validator import PathValidator

src_path = inquirer.filepath(
    message="Enter file to upload:",
    default=str(Path('/tmp')),
    validate=PathValidator(is_file=True, message="Input is not a file"),
    only_files=True,
).execute()

dest_path = inquirer.filepath(
    message="Enter path to download:",
    validate=PathValidator(is_dir=True, message="Input is not a directory"),
    only_directories=True,
).execute()

print(f'src_path: {src_path}, dest_path: {dest_path}')
```




## FuzzyPrompt()
入寮文字列のあいまい検索機能を持つリストプロンプトです。選択肢の数が約300kに達するまでは比較的良好な性能を発揮します。

```python
class FuzzyPrompt(BaseComplexPrompt):
    def __init__(
        self,
        message: Union[str, Callable[[SessionResult], str]],
        choices: Union[Callable[[SessionResult], List[Any]], List[Any]],
        default: Union[str, Callable[[SessionResult], str]] = "",
        pointer: str = INQUIRERPY_POINTER_SEQUENCE,
        style: InquirerPyStyle = None,
        vi_mode: bool = False,
        qmark: str = "?",
        transformer: Callable[[Any], Any] = None,
        filter: Callable[[Any], Any] = None,
        instruction: str = "",
        multiselect: bool = False,
        prompt: str = INQUIRERPY_POINTER_SEQUENCE,
        marker: str = INQUIRERPY_POINTER_SEQUENCE,
        border: bool = True,
        info: bool = True,
        height: Union[str, int] = None,
        max_height: Union[str, int] = None,
        validate: Union[Callable[[Any], bool], Validator] = None,
        invalid_message: str = "Invalid input",
        keybindings: Dict[str, List[Dict[str, Any]]] = None,
    ) -> None:
```


```pytohn:104_fuzzy.py
from contextlib import ExitStack
from pathlib import Path

from InquirerPy import inquirer, prompt

_WORDFILE_ = 'sample.txt'
def get_choices(_):
    p = Path.cwd().joinpath(_WORDFILE_)
    choices = []

    with ExitStack() as stack:
        if not p.exists():
            raise Exception('%s: wordfile missing!', _WORDFILE_)
        else:
            file = stack.enter_context(p.open("r"))
        for line in file.readlines():
            choices.append(line[:-1])
    return choices

action = inquirer.fuzzy(
    message="Select actions:",
    choices=["hello", "weather", "what", "whoa", "hey", "yo"],
    default="he",
    max_height="70%",
).execute()

words = inquirer.fuzzy(
    message="Select preferred words:",
    choices=get_choices,
    multiselect=True,
    validate=lambda result: len(result) > 1,
    invalid_message="minimum 2 selections",
    max_height="70%",
).execute()

print(f'action: {action}, words: {words}')
```



## PyInquirer との非互換について

InquierPy と PyInquirer とで互換性のないものについて説明します。

* editorプロンプト
inquirerpy は今時点では、editorプロンプトをサポートしていません。

* checkboxプロンプトのAPI
CheckBoxPrompt()のパラメタ名が異なっています。


 CheckboxPrompt()パラメタの比較

|  PyInquirer       |   InquirerPy        |
|:------------------|:--------------------|
|  pointer_sign     | pointer             |
|  selected_sign    | enabled_symbol      |
|  unselected_sign  | disabled_symbol     |


* Syle
PyInquirerのスタイルのキーのうち次のものは名前が変わっています。

| PyInquirer | InquirerPy
|:-----------|:--------------------|
| selected   | pointer             |



色やスタイルなどのカスタマイズはprompt_toolkit のスタイルを利用して行います。



## 所感
利用頻度は少ないかもしれませんが、対話型インタフェースをコマンドに実装するときに便利に使えるはずです。
`editor`など複数行入力などのインタフェースを使いたい場合は prompt_toolkit を使うことになるでしょう。
それでも、基本的な会話形インタフェースを構築するには十分な機能が提供されていて、見た目(Look&Feel) もシンプルで好感が持てます。

余談ですが、InquirerPy を使って[Emberblast](https://github.com/felipemeriga/Emberblast)
というクラシカルなRPGを作った人もいますね。


## 参考
 * [inquirerpy](ソースコード https://github.com/kazhala/InquirerPy)
 * [inquirerpy ドキュメント](https://github.com/kazhala/InquirerPy/wiki)
 * [FELIPE RAMOS ON BUILDING A PYTHON RPG GAME](https://x-team.com/blog/interview-felipe-ramos-da-silva/)
 * [Emberblast](https://github.com/felipemeriga/Emberblast)