はじめに
とあるCUIのアプリケーションを作ってて、100以上あるリストの中から8個を選択して入力するインタフェースが欲しいな、となった。
できればインタラクティブに、サクッと入力したい。どんな方法が一番いいかなぁ。
- 最小の入力と最小の視点移動で最大の効率を(最小の実装で)
- マウスとか矢印キーとかはできれば使いたくないなぁ
- 入力したいものの名前はだいたい覚えてる
入力シーンをイメージしてみる。
- 1個目の先頭の文字を入れる
- 意図した候補が表示される
- それでいい。リターン
- 2個目の入力。先頭の文字を入れる
- 思ってるのと違う候補が表示される
- それじゃない。2文字目、3文字目を入れる
- 意図した候補が表示される
- それだ。リターン
- 8回繰り返し
うーん、cursesかな。
cursesとは
簡単に言うと、文字やカーソルを自由自在に移動表示するための、テキストをベースとした技術。入力と出力で表示がどんどん上に流れていくのがよくある表示モードだとすると、cursesでは表示が流れずに入力、出力する位置を指定できるような場、機能を提供してくれる。
topコマンドやテキストエディタなんかも同じ技術をベースに表示とか入力を制御しているんじゃないかなぁ、知らんけど。
でも今でもcursesってあるのかなぁ。
グラフィカルなディスプレイが当たり前の今となっては、「なんでわざわざ?」と疑問に思うかもしれません。確かに文字表示端末は時代遅れな技術ではありますが、ニッチな領域が存在していて、意匠を凝らすことができるため、いまだに価値のあるものとなっています。
(https://docs.python.org/ja/3/howto/curses.html)
ふーん、そうなんだ。で、今回の環境はWindows7+Python3。
Pythonはcursesを標準装備してるはずなのにエラーが出るので調べてみると、
Windows 版 Python には curses が含まれていません。 UniCurses という名の移植版が利用可能です。Fredrik Lundh の手による the Console module も試してみると良いでしょう。
(https://docs.python.org/ja/3/howto/curses.html)
なんでやねん… 紹介されてる2つの実装もいろいろと不都合があって諦めかけるも、windows-cursesという実装がPyPiに!
せんきゅー、PyPi!
実装
最終的なコードは以下のとおり。
import curses
import bukisp
import re
yomis = [buki['yomi'] for buki in bukisp.bukis]
def main(stdscr):
selected = []
window = curses.initscr()
for i in range(8):
key = ''
input_str = ''
choices = []
stdscr.clear()
stdscr.addstr(0,0,'%d番目のブキ:' % (i+1))
while key != '\n':
stdscr.refresh()
if len(input_str) == 0:
choices = ['???']
else:
choices = [s for s in yomis if re.match(input_str, s)]
if len(choices) == 0: choices = ['???']
stdscr.addstr(0,8,choices[0])
window.clrtobot()
window.attrset(curses.A_REVERSE)
stdscr.addstr(0,8,input_str)
window.attrset(curses.A_NORMAL)
window.move(0,13+len(input_str))
key = stdscr.getkey()
if key == '\x08':
input_str = input_str[:-1]
else:
input_str += key
selected.append(choices[0])
continue
if __name__ == '__main__':
curses.wrapper(main)
ちなみに候補リスト(bukisp.py
)は以下のような構造です。
bukis = [
{'name':'わかば', 'cate':cates[0], 'sub':subs[1], 'sp':sps[0], 'yomi':'wakaba'},
{'name':'もみじ', 'cate':cates[0], 'sub':subs[3], 'sp':sps[2], 'yomi':'momiji'},
# : (150個くらい)
]
操作例
1番目のブキ: ???
1番目のブキ: susi
(実際は入力した「s」のみ反転表示。)
2番目のブキ: ???
2番目のブキ: momiji
(実際は入力した「m」のみ反転表示。)
2番目のブキ: manyu
(実際は入力した「ma」のみ反転表示。)
3番目のブキ: ???
8回繰り返すと終了。selected
リストに選択した'susi'
などが格納される。だいたいイメージどおりにできた。久々に使ってみたけど、curses便利だな。
実装メモ
2時間くらいここら辺 (https://docs.python.org/ja/3/library/curses.html?highlight=curses#module-curses) と格闘したレベル。はっきりいってウィンドウとかスクリーンとか全然理解してません。
import curses
def main(stdscr):
stdscr.clear()
stdscr.addstr(0,0,'1番目のブキ:')
key = stdscr.getkey()
if __name__ == '__main__':
curses.wrapper(main)
これがベース。初期化とかをwrapper()
がやってくれるのでお約束。main
にstdscr
というオブジェクトが渡される。stdscr.getkey()
でキーボードからの入力待ちをする。そうしないとプログラムが即座に終了して表示も消え、あたかもなにも起きなかったかのような錯覚に陥ってしまう。
上記はキーボードから何か1文字入力すると終了する。
とりあえず入力を8回繰り返すようにしよう。
def main(stdscr):
for i in range(8):
stdscr.clear()
stdscr.addstr(0,0,'%d番目のブキ:' % i)
key = stdscr.getkey()
1文字入力したら次にいっちゃうんで、リターンキーが入力されるまでは連続して入力できるようにしたい。
def main(stdscr):
for i in range(8):
key = ''
input_str = ''
stdscr.addstr(0,0,'%d番目のブキ:' % (i+1))
while key != '\n':
key = stdscr.getkey()
input_str += key
continue
Win7の場合、上記のように\n
でリターンキーを指定できるようです。
ただ、リターンキーを打つまでは連続で入力を受け付けてくれるのに、リターンキーを打つとfor
を抜けて終了してしまうので、continue
を入れてます。
これだけだと実際に入力した文字が表示されないので、input_str
と入力位置を示すカーソルをstdscr.addstr()
とwindow.move()
で表示する。
window = curses.initscr()
:
while key != '\n':
stdscr.addstr(0,8,input_str)
window.move(0,13+len(input_str))
key = stdscr.getkey()
input_str += key
入力ミスは修正したいので、バックスペースの処理を追加。
while key != '\n':
stdscr.addstr(0,8,input_str)
window.move(0,13+len(input_str))
key = stdscr.getkey()
if key == '\x08':
input_str = input_str[:-1]
else:
input_str += key
Win7の場合、^H
もBACKSPACE
も\x08
で拾えるみたい。
入力系はだいたいできたので、次に候補表示を実装。もともとのリストは、
bukis = [
{'name':'わかば', 'cate':cates[0], 'sub':subs[1], 'sp':sps[0]},
{'name':'もみじ', 'cate':cates[0], 'sub':subs[3], 'sp':sps[2]},
# : (150個くらい)
]
これに、入力に対応するよう、yomi
というキーを追加。
bukis = [
{'name':'わかば', 'cate':cates[0], 'sub':subs[1], 'sp':sps[0], 'yomi':'wakaba'},
{'name':'もみじ', 'cate':cates[0], 'sub':subs[3], 'sp':sps[2], 'yomi':'momiji'},
# : (150個くらい)
]
プログラムの最初の方で、yomi
だけyomis
リストに詰め込みます。
各回の入力に従って、入力文字列(input_str
)に先頭マッチする候補だけをchoices
リストに入れ、先頭の候補を入力中のカーソルの位置に表示します。候補表示と入力文字列をわかりやすくするため、attrset()
で入力中の文字列のみ反転表示します。
import re
import bukisp
yomis = [buki['yomi'] for buki in bukisp.bukis]
:
while key != '\n':
stdscr.refresh()
if len(input_str) == 0:
choices = ['???']
else:
choices = [s for s in yomis if re.match(input_str, s)]
stdscr.addstr(0,8,choices[0])
window.clrtobot()
window.attrset(curses.A_REVERSE)
stdscr.addstr(0,8,input_str)
window.attrset(curses.A_NORMAL)
以上。