curses を用いた TUI ダイアログの作成

呼称: curses によるダイアログメニュー
目的: TUI のダイアログメニューを作成する
特徴: ガリガリちから技で座標位置やキー入力を処理
用例: GUI のない環境でメニューを表示する
備考: dialog コマンドでも代用可

pythoncurses ライブラリを使用してメニューダイアログを作成してみました。
この程度であれば、普通に dialog コマンドを使用すれば良いです。しかし、何らかの理由で dialog が使用できない、又はこだわって全て python で書きたいときに有効です(^ ^;;

ターミナルの座標制御やキー入力の処理を簡潔に書けると、もう少し汎用的に使用できるかも?もう一工夫、必要ですね。

#!/usr/bin/env python
"""
Text User Interface with Curses
"""

import curses
import sys

menu_frame = """\
  +----------------------------------------+
  | This menu is sample for Curses         |
  | select item which you want             |
  +----------------------------------------+
  |   [ ]: 1st Item   (return 'first')     |
  |   [ ]: 2nd Item   (return 'second')    |
  |   [ ]: 3rd Item   (return 'third')     |
  |   [ ]: 4th Item   (return 'fourth')    |
  |   [ ]: 5th Item   (return 'fifth')     |
  |   type "a": select/unselect all items  |
  +----------------------------------------+
  |          < OK >      < Quit >          |
  +----------------------------------------+"""

class CursesMenu:
    """ define terminal position """
    def __init__(self):
        self.x_ast = 7
        self.x_ok = 13
        self.x_quit = 25
        self.x_max = 44
        self.y_hdr = 0
        self.y_1st = 4
        self.y_2nd = 5
        self.y_3rd = 6
        self.y_4th = 7
        self.y_5th = 8
        self.y_btn = 11
        self.y_max = 12
        self.item = {}

    def set_menu(self):
        self.scr.addstr(self.y_hdr, 0, menu_frame)

    def display_menu(self, scr):
        def ret_item(item, r=[]):
            l = item.items(); l.sort()
            for i in [x[0] for x in l if x[1] == '*']:
                if   i == self.y_1st: r.append('first')
                elif i == self.y_2nd: r.append('second')
                elif i == self.y_3rd: r.append('third')
                elif i == self.y_4th: r.append('fourth')
                elif i == self.y_5th: r.append('fifth')
            return r
            
        def reverse_item(y, c='*'):
            if self.scr.instr(y, self.x_ast, 1) == '*': c=' '
            self.scr.addch(y, self.x_ast, c)
            self.item[y] = c
    
        def reverse_all_item(c='*'):
            if self.scr.instr(self.y_1st, self.x_ast, 1) == '*': c = ' '
            for y in [self.y_1st, self.y_2nd, self.y_3rd, self.y_4th, self.y_5th]:
                self.scr.addch(y, self.x_ast, c)
                self.item[y] = c
    
        def change_bgcolor(y, x):
            self.scr.addstr(self.y_btn, 0,
                '  |          < OK >      < Quit >          |')
            if y == self.y_btn:
                if x == self.x_ok:
                    self.scr.addstr(self.y_btn, self.x_ok, 
                        '< OK >', curses.color_pair(1))
                elif x == self.x_quit:
                    self.scr.addstr(self.y_btn, self.x_quit, 
                        '< Quit >', curses.color_pair(1))
    
        def move_by_tab():
            y, x = self.scr.getyx()
            if x == self.x_ast:
                y = self.y_btn
                x = self.x_ok
            elif x == self.x_ok:
                y = self.y_btn
                x = self.x_quit
            else: 
                y = self.y_1st
                x = self.x_ast
            return y, x
        
        # clear the screen and set menu and keys
        self.scr = scr
        self.scr.clear()
        scr_y, scr_x = self.scr.getmaxyx()
        
        # check max terminal size
        if scr_y < self.y_max or scr_x < self.x_max:
            curses.endwin()
            print "Cannot display Menu in this size"
            sys.exit(0)
        
        # initialize color setting if enable
        if curses.has_colors():
            curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
        
        # set initial cursor position
        cur_y = self.y_1st
        cur_x = self.x_ast
        self.set_menu()
        # set all items by default
        reverse_all_item()

        # get key input from user
        while True:
            # change color setting if enable
            if curses.has_colors():
                change_bgcolor(cur_y, cur_x)
            self.scr.move(cur_y, cur_x)
            c = self.scr.getch()
            if 0 < c < 256:   # normal character
                c = chr(c)
                if c in ' \n':
                    y, x = self.scr.getyx()
                    if x == self.x_ok:
                        return ret_item(self.item)
                    elif x == self.x_quit:
                        sys.exit(0)
                    elif x == self.x_ast:
                        reverse_item(y)
                elif c in '\t':
                    cur_y, cur_x = move_by_tab()
                elif c in 'aA':
                    reverse_all_item()
                elif c in '1':
                    reverse_item(self.y_1st)
                elif c in '2':
                    reverse_item(self.y_2nd)
                elif c in '3':
                    reverse_item(self.y_3rd)
                elif c in '4':
                    reverse_item(self.y_4th)
                elif c in '5':
                    reverse_item(self.y_5th)
                elif c in 'oO':
                    cur_y, cur_x = self.y_btn, self.x_ok
                elif c in 'qQ':
                    cur_y, cur_x = self.y_btn, self.x_quit
                else: pass   # Ignore incorrect keys
            elif c == curses.KEY_UP and self.y_1st < cur_y:
                cur_x = self.x_ast
                if cur_y == self.y_btn: cur_y -= 2
                cur_y -= 1
            elif c == curses.KEY_DOWN and cur_y < self.y_btn:
                cur_x = self.x_ast
                if cur_y == self.y_5th: 
                    cur_x = self.x_ok
                    cur_y += 2
                cur_y += 1
            elif c == curses.KEY_LEFT and cur_y == self.y_btn:
                cur_x = self.x_ok
            elif c == curses.KEY_RIGHT and cur_y == self.y_btn:
                cur_x = self.x_quit
            else: pass   # Ignore incorrect keys

""" initialize and call menu function """
def main():
    item = curses.wrapper(CursesMenu().display_menu)
    print item

if __name__ == '__main__':
    main()

実行結果。

  +----------------------------------------+
  | This menu is sample for Curses         |
  | select item which you want             |
  +----------------------------------------+
  |   [*]: 1st Item   (return 'first')     |
  |   [ ]: 2nd Item   (return 'second')    |
  |   [*]: 3rd Item   (return 'third')     |
  |   [ ]: 4th Item   (return 'fourth')    |
  |   [*]: 5th Item   (return 'fifth')     |
  |   type "a": select/unselect all items  |
  +----------------------------------------+
  |          < OK >      < Quit >          |
  +----------------------------------------+

OK を選択して Enter
['first', 'third', 'fifth']

リファレンス:
14.7 curses -- 文字セル表示のための端末操作
PythonにおけるCursesプログラミング