#!/usr/bin/env python3 # coding: utf-8 # This code is available for use under CC0 (Creative Commons 0 - universal). # You can copy, modify, distribute and perform the work, even for commercial # purposes, all without asking permission. For more information, see LICENSE.md or # https://creativecommons.org/publicdomain/zero/1.0/ # usage: # opts = Picker( # title = 'Delete all files', # options = ["Yes", "No"] # ).get_selected() # returns a simple list # cancel returns False import curses from curses import wrapper import locale locale.setlocale(locale.LC_ALL, "") class Picker(object): """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 = 80 all_options = [] length = 0 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() 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 get_selected(self): if self.aborted: return False ret_s = filter(lambda x: x["selected"], self.all_options) return list(map(lambda x: x["label"], ret_s)) 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"]: line_label = self.c_selected + " " else: line_label = self.c_empty + " " self.win.addstr(position + 2, 5, (line_label + option["label"]).encode('utf-8')) 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 -= 1 def check_cursor_down(self): if self.cursor >= self.length: self.cursor -= 1 if self.cursor > self.window_height: self.cursor = self.window_height self.offset += 1 if self.offset + self.cursor >= self.length: 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 -= 1 elif c == curses.KEY_DOWN: 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.get_selected() self.selcount = len(temp)