16
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

n十年ぶりにcurses使ったらやっぱいいじゃんっていう話

Last updated at Posted at 2019-07-12

はじめに

とある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番目のブキ: ???
先頭の文字(s)を入力
1番目のブキ: susi

(実際は入力した「s」のみ反転表示。)

susiでおっけー。リターンを入力
2番目のブキ: ???
先頭の文字(m)を入力
2番目のブキ: momiji

(実際は入力した「m」のみ反転表示。)

momijiじゃない。2文字め(a)を入力
2番目のブキ: manyu

(実際は入力した「ma」のみ反転表示。)

manyuでおっけー。リターンを入力
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()がやってくれるのでお約束。mainstdscrというオブジェクトが渡される。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の場合、^HBACKSPACE\x08で拾えるみたい。

入力系はだいたいできたので、次に候補表示を実装。もともとのリストは、

bukis = [
    {'name':'わかば', 'cate':cates[0], 'sub':subs[1], 'sp':sps[0]},
    {'name':'もみじ', 'cate':cates[0], 'sub':subs[3], 'sp':sps[2]},
    # : (150個くらい)
]

これに、入力に対応するよう、yomiというキーを追加。

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個くらい)
]

プログラムの最初の方で、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)

以上。

16
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?