pythonのCLIでリスト選択のUIにemacs/shell風のキーバインドを追加してみる
なんとなくCLIのツールを作ろうかと思って色々試してみていた。python-inquirerというものを発見したので使ってみていた。まだこれを常用するかは決めていない(元々inquirerはnode.js用のライブラリだった模様。それのpython版がこれ。yeomanなどに使われていたらしいのでちょっと古めのライブラリかも?ちなみにinquirerとは別にenquirerというものもありnpmはむずかしい)。
とりあえず触ってみてemacs風のキーバインドで操作したくなったのでコードを改変してみた。
インストール
pip install inquirer
で使えるようになる。
リスト選択UI
試しにサンプルの通りにリスト用のUIのAPIを作ってみる(examplesから拝借)
import inquirer questions = [ inquirer.List( "size", message="What size do you need?", choices=["Jumbo", "Large", "Standard", "Medium", "Small", "Micro"], carousel=True, ) ] answers = inquirer.prompt(questions) print(answers)
こんな感じの表示。
上下のカーソルキーで選べる。
emacs風のキーバインドで動かしたい
ところで便利は便利なのだけど、リスト選択でemacs風のキーバインドが使いたくなる。具体的には以下の様な操作。
Ctrl f
もしくはCtrl n
で次へ、Ctrl b
もしくはCtrl p
で前へCtrl a
で先頭(行末)にCtrl e
で末尾(行末)に
これは以下のようにちょっと頑張ってあげるとできる。
import inquirer import string from inquirer import themes from inquirer.render.console import ConsoleRender, List from readchar import key class ExtendedConsoleRender(ConsoleRender): def render_factory(self, question_type): if question_type == "list": return ExtendedList return super().render_factory(question_type) # CTRL_MAP["B"]はkey.CTRL_Bでも良いのだけれどAからZまで全部は定義されていなかったので CTRL_MAP = {c: chr(i) for i, c in enumerate(string.ascii_uppercase, 1)} class ExtendedList(List): def process_input(self, pressed): # emacs style if pressed in (CTRL_MAP["B"], CTRL_MAP["P"]): pressed = key.UP elif pressed in (CTRL_MAP["F"], CTRL_MAP["N"]): pressed = key.DOWN elif pressed == CTRL_MAP["G"]: pressed = CTRL_MAP["C"] elif pressed == CTRL_MAP["A"]: self.current = 0 return elif pressed == CTRL_MAP["G"]: self.current = len(self.question.choices) - 1 return # vi style if pressed in ("k", "h"): pressed = key.UP elif pressed in ("j", "l"): pressed = key.DOWN elif pressed == "q": pressed = key.CTRL_C # effect (rendering) super().process_input(pressed) questions = [ inquirer.List( "size", message="What size do you need?", choices=["Jumbo", "Large", "Standard", "Medium", "Small", "Micro"], carousel=True, ) ] answers = inquirer.prompt(questions, render=ExtendedConsoleRender(theme=themes.GreenPassion())) print(answers)
ちなみにモンキーパッチでも対応可。
むずかしいはなし
さっきのような感じで動くものはできたんだけれど、こういうCLI用のライブラリにどう対応するかと言うのがむずかしいなーと思ったりした。
- 使うところのすべてで拡張部分コピペしたくない
- 新たに小さくwrapperライブラリを用意したくない
- ライブラリにPR出そうにもnode.js用のライブラリのport、独自の機能にはしたくない
という感じで悩ましい。