selector.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. #!/usr/bin/env python
  2. import os
  3. import curses
  4. import curses.wrapper
  5. import locale
  6. import pprint
  7. import textwrap
  8. KEY_ESC = 27
  9. KEY_SDOWN = 336
  10. KEY_SUP = 337
  11. class Node(object):
  12. def __init__(self):
  13. self._parent = None
  14. self._children = []
  15. self._selected = False
  16. pass
  17. def child_count(self):
  18. return len(self._children)
  19. def find_root(self):
  20. if self._parent is None:
  21. return self
  22. else:
  23. return self._parent.find_root()
  24. def find_selected(self):
  25. selected_nodes = []
  26. if self.selected():
  27. selected_nodes.append(self)
  28. for child in self.get_children():
  29. selected_nodes = selected_nodes + child.find_selected()
  30. return selected_nodes
  31. def get_label(self, active_root):
  32. return ''
  33. def _clear_children(self):
  34. self._children = []
  35. def get_children(self):
  36. return self._children
  37. def _append_child(self, child):
  38. child._parent = self
  39. self._children.append(child)
  40. def get_parent(self):
  41. return self._parent
  42. def get_header_height(self):
  43. return 0
  44. def select(self):
  45. self._selected = True
  46. def unselect(self):
  47. self._selected = False
  48. def toggle(self):
  49. if self.selected():
  50. self.unselect()
  51. else:
  52. self.select()
  53. def selected(self):
  54. return self._selected
  55. class StaticNode(Node):
  56. def __init__(self, label):
  57. super(StaticNode, self).__init__()
  58. self.label = label
  59. def append_child(self, child):
  60. self._append_child(child)
  61. def get_label(self):
  62. return self.label
  63. class SelectionPad(object):
  64. def __init__(self, parent_window):
  65. self._pad = curses.newpad(1, 1)
  66. self._pad.keypad(1)
  67. def refresh(self, pminrow, sminrow, smaxrow, smaxcol):
  68. # window.refresh([pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol])
  69. # pminrow and pmincol specify the upper left-hand corner of the rectangle to be displayed in the pad.
  70. # sminrow, smincol, smaxrow, and smaxcol specify the edges of the rectangle to be displayed on the screen.
  71. # The lower right-hand corner of the rectangle to be displayed in the pad is calculated from the screen coordinates, since the rectangles must be the same size.
  72. self._pad.refresh(pminrow, 0, sminrow, 0, smaxrow, smaxcol)
  73. def addstr(self, line_index, col_index, text, attr = 0):
  74. text_encoded = text.encode(locale.getpreferredencoding())
  75. try:
  76. self._pad.addstr(line_index, col_index, text_encoded, attr)
  77. except Exception, ex:
  78. raise Exception('pad(width=%d, height=%d).addstr(%d, %d, %s, %d) failed'
  79. % (self.get_width(), self.get_height(), line_index, col_index, repr(text_encoded), attr))
  80. def clear(self):
  81. self._pad.clear()
  82. def get_height(self):
  83. return self._pad.getmaxyx()[0]
  84. def get_width(self):
  85. return self._pad.getmaxyx()[1]
  86. def getch(self):
  87. return self._pad.getch()
  88. def resize(self, nlines, ncols):
  89. assert nlines > 0
  90. assert ncols > 0
  91. try:
  92. self._pad.resize(nlines, ncols)
  93. except Exception, ex:
  94. raise Exception('pad(width=%d, height=%d).resize(%d, %d) failed'
  95. % (self.get_width(), self.get_height(), nlines, ncols))
  96. def resize_height(self, nlines):
  97. self.resize(nlines, self.get_width())
  98. def resize_width(self, ncols):
  99. self.resize(self.get_height(), ncols)
  100. def select(stdscr, active_root, multiple = False):
  101. curses.curs_set(0)
  102. pad = SelectionPad(stdscr)
  103. active_index = 0
  104. def get_screen_height():
  105. return stdscr.getmaxyx()[0]
  106. def get_active_node():
  107. if active_index < len(active_root.get_children()):
  108. return active_root.get_children()[active_index]
  109. else:
  110. return None
  111. def get_visible_pad_height():
  112. return get_screen_height() - active_root.get_header_height()
  113. def refresh():
  114. pad.clear()
  115. pad.resize_width(1)
  116. if len(active_root.get_children()) > 0:
  117. pad.resize_height(active_root.child_count())
  118. for child_index in range(len(active_root.get_children())):
  119. child = active_root.get_children()[child_index]
  120. label = child.get_label()
  121. # if the last line is the longest addstr() fails if
  122. # the width does not include one additional char.
  123. if len(label) > pad.get_width():
  124. pad.resize_width(len(label) + 1)
  125. pad.addstr(
  126. child_index,
  127. 0,
  128. label,
  129. (curses.A_UNDERLINE if child_index == active_index else 0)
  130. | (curses.A_BOLD if child.selected() else 0)
  131. )
  132. pad.refresh(
  133. max(
  134. 0,
  135. min(
  136. active_root.child_count() - get_visible_pad_height(),
  137. active_index - int(get_visible_pad_height() / 2)
  138. )
  139. ),
  140. active_root.get_header_height(),
  141. get_screen_height() - 1,
  142. stdscr.getmaxyx()[1] - 1
  143. )
  144. while True:
  145. refresh()
  146. try:
  147. key = pad.getch()
  148. except KeyboardInterrupt:
  149. return None
  150. active_node = get_active_node()
  151. if key == curses.KEY_RESIZE:
  152. refresh()
  153. elif key in [curses.KEY_DOWN, KEY_SDOWN, ord('j'), ord('J')]:
  154. if multiple and key in [KEY_SDOWN, ord('J')]:
  155. active_node.toggle()
  156. active_index = min(active_root.child_count() - 1, active_index + 1)
  157. elif key in [curses.KEY_UP, KEY_SUP, ord('k'), ord('K')]:
  158. if multiple and key in [KEY_SUP, ord('K')]:
  159. active_node.toggle()
  160. active_index = max(0, active_index - 1)
  161. elif key in [curses.KEY_NPAGE, ord('f') - ord('a') + 1]: # control-f
  162. active_index = min(active_root.child_count() - 1, active_index + int(get_visible_pad_height() / 2))
  163. elif key in [curses.KEY_PPAGE, ord('b') - ord('a') + 1]: # control-b
  164. active_index = max(0, active_index - int(get_visible_pad_height() / 2))
  165. elif active_node and key in [ord(' ')]:
  166. if multiple:
  167. active_node.toggle()
  168. else:
  169. active_node.select()
  170. return active_root.find_root().find_selected()
  171. elif active_node and key in [ord('\n')]:
  172. active_node.select()
  173. return active_root.find_root().find_selected()
  174. elif key in [ord('q'), KEY_ESC]:
  175. return None
  176. # else:
  177. # raise Exception(key)
  178. def select_string(stdscr, strings, multiple = False):
  179. root = StaticNode('root')
  180. for string in strings:
  181. root.append_child(StaticNode(string))
  182. selection = select(stdscr, root, multiple = multiple)
  183. if selection is None:
  184. return None
  185. else:
  186. return [n.get_label() for n in selection]