Current File : //opt/imh-python/lib/python2.7/site-packages/picker.py
#!/usr/bin/python

# hi, this routine isn't licensed. but credit would be nice somewhere.
# See http://pp19dd.com/2013/11/simple-python-list-picker-using-curses/

# usage:
# opts = Picker(
#    title = 'Delete all files',
#    options = ["Yes", "No"]
# ).getSelected()

# returns a simple list
# cancel returns False

import curses
import curses.wrapper

class Picker:
    """Allows you to select from a list with curses"""
    stdscr = None
    win = None
    title = ""
    arrow = ""
    footer = ""
    more = ""
    c_selected = ""
    c_empty = ""
    
    cursor = 0
    offset = 0
    selected = 0
    selcount = 0
    aborted = False
    
    window_height = 15
    window_width = 60
    all_options = []
    length = 0
    
    def curses_start(self):
        self.stdscr = curses.initscr()
        curses.noecho()
        curses.cbreak()
        self.win = curses.newwin(
            5 + self.window_height,
            self.window_width,
            2,
            4
        )
    
    def curses_stop(self):
        curses.nocbreak()
        self.stdscr.keypad(0)
        curses.echo()
        curses.endwin()

    def getSelected(self):
        if self.aborted == True:
            return( False )

        ret_s = filter(lambda x: x["selected"], self.all_options)
        ret = map(lambda x: x["label"], ret_s)
        return( ret )
        
    def redraw(self):
        self.win.clear()
        self.win.border(
            self.border[0], self.border[1],
            self.border[2], self.border[3],
            self.border[4], self.border[5],
            self.border[6], self.border[7]
        )
        self.win.addstr(
            self.window_height + 4, 5, " " + self.footer + " "
        )
        
        position = 0
        range = self.all_options[self.offset:self.offset+self.window_height+1]
        for option in range:
            if option["selected"] == True:
                line_label = self.c_selected + " "
            else:
                line_label = self.c_empty + " "
            
            self.win.addstr(position + 2, 5, line_label + option["label"])
            position = position + 1
            
        # hint for more content above
        if self.offset > 0:
            self.win.addstr(1, 5, self.more)
        
        # hint for more content below
        if self.offset + self.window_height <= self.length - 2:
            self.win.addstr(self.window_height + 3, 5, self.more)
        
        self.win.addstr(0, 5, " " + self.title + " ")
        self.win.addstr(
            0, self.window_width - 8,
            " " + str(self.selcount) + "/" + str(self.length) + " "
        )
        self.win.addstr(self.cursor + 2,1, self.arrow)
        self.win.refresh()

    def check_cursor_up(self):
        if self.cursor < 0:
            self.cursor = 0
            if self.offset > 0:
                self.offset = self.offset - 1
    
    def check_cursor_down(self):
        if self.cursor >= self.length:
            self.cursor = self.cursor - 1
    
        if self.cursor > self.window_height:
            self.cursor = self.window_height
            self.offset = self.offset + 1
            
            if self.offset + self.cursor >= self.length:
                self.offset = self.offset - 1
    
    def curses_loop(self, stdscr):
        while 1:
            self.redraw()
            c = stdscr.getch()
            
            if c == ord('q') or c == ord('Q'):
                self.aborted = True
                break
            elif c == curses.KEY_UP:
                self.cursor = self.cursor - 1
            elif c == curses.KEY_DOWN:
                self.cursor = self.cursor + 1
            #elif c == curses.KEY_PPAGE:
            #elif c == curses.KEY_NPAGE:
            elif c == ord(' '):
                self.all_options[self.selected]["selected"] = \
                    not self.all_options[self.selected]["selected"]
            elif c == 10:
                break
                    
            # deal with interaction limits
            self.check_cursor_up()
            self.check_cursor_down()

            # compute selected position only after dealing with limits
            self.selected = self.cursor + self.offset
            
            temp = self.getSelected()
            self.selcount = len(temp)
    
    def __init__(
        self, 
        options, 
        title='Select', 
        arrow="-->",
        footer="Space = toggle, Enter = accept, q = cancel",
        more="...",
        border="||--++++",
        c_selected="[X]",
        c_empty="[ ]"
    ):
        self.title = title
        self.arrow = arrow
        self.footer = footer
        self.more = more
        self.border = border
        self.c_selected = c_selected
        self.c_empty = c_empty
        
        self.all_options = []
        
        for option in options:
            self.all_options.append({
                "label": option,
                "selected": False
            })
            self.length = len(self.all_options)
        
        self.curses_start()
        curses.wrapper( self.curses_loop )
        self.curses_stop()