|
@@ -0,0 +1,167 @@
|
|
|
+#!/usr/bin/env python
|
|
|
+
|
|
|
+import os
|
|
|
+import curses
|
|
|
+import curses.textpad
|
|
|
+import curses.wrapper
|
|
|
+import textwrap
|
|
|
+
|
|
|
+class Node(object):
|
|
|
+
|
|
|
+ def __init__(self):
|
|
|
+ self._parent = None
|
|
|
+ self._children = []
|
|
|
+ self._selected = False
|
|
|
+ pass
|
|
|
+
|
|
|
+ def child_count(self):
|
|
|
+ return len(self._children)
|
|
|
+
|
|
|
+ def find_root(self):
|
|
|
+ if self._parent is None:
|
|
|
+ return self
|
|
|
+ else:
|
|
|
+ return self._parent.find_root()
|
|
|
+
|
|
|
+ def find_selected(self):
|
|
|
+ selected_nodes = []
|
|
|
+ if self.selected():
|
|
|
+ selected_nodes.append(self)
|
|
|
+ for child in self.get_children():
|
|
|
+ selected_nodes = selected_nodes + child.find_selected()
|
|
|
+ return selected_nodes
|
|
|
+
|
|
|
+ def get_label(self, active_root):
|
|
|
+ return 'node'
|
|
|
+
|
|
|
+ def get_children(self):
|
|
|
+ return self._children
|
|
|
+
|
|
|
+ def append_child(self, child):
|
|
|
+ child._parent = self
|
|
|
+ self._children.append(child)
|
|
|
+
|
|
|
+ def select(self):
|
|
|
+ self._selected = True
|
|
|
+
|
|
|
+ def selected(self):
|
|
|
+ return self._selected
|
|
|
+
|
|
|
+class StaticNode(Node):
|
|
|
+
|
|
|
+ def __init__(self, label):
|
|
|
+ super(StaticNode, self).__init__()
|
|
|
+ self.label = label
|
|
|
+
|
|
|
+ def get_label(self):
|
|
|
+ return self.label
|
|
|
+
|
|
|
+class SelectionPad(object):
|
|
|
+
|
|
|
+ def __init__(self, parent_window):
|
|
|
+ self._pad = curses.newpad(1, 1)
|
|
|
+ self._pad.keypad(1)
|
|
|
+
|
|
|
+ def refresh(self, pminrow, smaxrow, smaxcol):
|
|
|
+ # window.refresh([pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol])
|
|
|
+ # pminrow and pmincol specify the upper left-hand corner of the rectangle to be displayed in the pad.
|
|
|
+ # sminrow, smincol, smaxrow, and smaxcol specify the edges of the rectangle to be displayed on the screen.
|
|
|
+ # 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.
|
|
|
+ self._pad.refresh(pminrow, 0, 0, 0, smaxrow, smaxcol)
|
|
|
+
|
|
|
+ def addstr(self, line_index, col_index, text, attr = 0):
|
|
|
+ self._pad.addstr(line_index, col_index, text, attr)
|
|
|
+
|
|
|
+ def clear(self):
|
|
|
+ self._pad.clear()
|
|
|
+
|
|
|
+ def get_height(self):
|
|
|
+ return self._pad.getmaxyx()[0]
|
|
|
+
|
|
|
+ def get_width(self):
|
|
|
+ return self._pad.getmaxyx()[1]
|
|
|
+
|
|
|
+ def getch(self):
|
|
|
+ return self._pad.getch()
|
|
|
+
|
|
|
+ def resize(self, nlines, ncols):
|
|
|
+ self._pad.resize(nlines, ncols)
|
|
|
+
|
|
|
+ def resize_height(self, nlines):
|
|
|
+ self.resize(nlines, self.get_width())
|
|
|
+
|
|
|
+ def resize_width(self, ncols):
|
|
|
+ self.resize(self.get_height(), ncols)
|
|
|
+
|
|
|
+def select(stdscr, active_root):
|
|
|
+
|
|
|
+ curses.curs_set(0)
|
|
|
+ pad = SelectionPad(stdscr)
|
|
|
+
|
|
|
+ active_index = 0
|
|
|
+
|
|
|
+ def get_screen_height():
|
|
|
+ return stdscr.getmaxyx()[0]
|
|
|
+
|
|
|
+ def refresh():
|
|
|
+ pad.clear()
|
|
|
+ pad.resize_width(1)
|
|
|
+ pad.resize_height(len(active_root.get_children()))
|
|
|
+ for child_index in range(len(active_root.get_children())):
|
|
|
+ child = active_root.get_children()[child_index]
|
|
|
+ label = child.get_label()
|
|
|
+ if len(label) > pad.get_width():
|
|
|
+ pad.resize_width(len(label))
|
|
|
+ pad.addstr(
|
|
|
+ child_index,
|
|
|
+ 0,
|
|
|
+ label,
|
|
|
+ (curses.A_UNDERLINE if child_index == active_index else 0)
|
|
|
+ | (curses.A_BOLD if child.selected() else 0)
|
|
|
+ )
|
|
|
+ pad.refresh(
|
|
|
+ max(
|
|
|
+ 0,
|
|
|
+ min(
|
|
|
+ active_root.child_count() - get_screen_height(),
|
|
|
+ active_index - int(get_screen_height() / 2)
|
|
|
+ )
|
|
|
+ ),
|
|
|
+ get_screen_height() - 1,
|
|
|
+ stdscr.getmaxyx()[1] - 1
|
|
|
+ )
|
|
|
+
|
|
|
+ while True:
|
|
|
+ refresh()
|
|
|
+ try:
|
|
|
+ key = pad.getch()
|
|
|
+ except KeyboardInterrupt:
|
|
|
+ return None
|
|
|
+ if key == curses.KEY_RESIZE:
|
|
|
+ refresh()
|
|
|
+ elif key in [curses.KEY_DOWN, ord('j')]:
|
|
|
+ active_index = min(active_root.child_count() - 1, active_index + 1)
|
|
|
+ elif key in [curses.KEY_UP, ord('k')]:
|
|
|
+ active_index = max(0, active_index - 1)
|
|
|
+ elif key == curses.KEY_NPAGE:
|
|
|
+ active_index = min(active_root.child_count() - 1, active_index + int(get_screen_height() / 2))
|
|
|
+ elif key == curses.KEY_PPAGE:
|
|
|
+ active_index = max(0, active_index - get_screen_height())
|
|
|
+ elif key in [ord(' '), ord('\n')]:
|
|
|
+ active_root.get_children()[active_index].select()
|
|
|
+ return active_root.find_root().find_selected()
|
|
|
+ else:
|
|
|
+ raise Exception(key)
|
|
|
+
|
|
|
+def select_lorem():
|
|
|
+ import random
|
|
|
+ import string
|
|
|
+ root = StaticNode('root')
|
|
|
+ for i in range(random.randint(128, 128)):
|
|
|
+ root.append_child(StaticNode(str(i).ljust(5) + ''.join(random.choice(string.lowercase + string.uppercase + ' ') for i in range(random.randint(50, 160)))))
|
|
|
+ selection = curses.wrapper(select, root)
|
|
|
+ if selection is None:
|
|
|
+ print('aborted')
|
|
|
+ else:
|
|
|
+ for node in selection:
|
|
|
+ print(node.label)
|