Allow to select files and collections. Handle long status lines.
[pysize.git] / pysize / ui / curses / ui_curses.py
blob71a6700f85a998c4a3d1a3c8ee4e24b514e461b1
1 import curses
2 import os
3 import sys
4 import time
6 from pysize.ui.utils import human_unit, short_string, update_progress
7 from pysize.ui.utils import sanitize_string
8 from pysize.ui.char_matrix import CharMatrix, MASK, SELECTED
9 from pysize.core.pysize_fs_tree import pysize_tree
10 from pysize.core.compute_size import size_observable
12 class _CursesApp(object):
13 """The curses UI."""
14 def __init__(self, win, options, args):
15 self.window = win
16 self.max_depth = options.max_depth
17 if options.min_size != 'auto':
18 raise Exception, 'curses UI supports only --min-size=auto'
19 self._set_paths(args)
20 self._init_curses()
21 self.last_star = 0
22 self.last_star_time = time.time()
23 size_observable.add_observer(self.draw_star)
25 def draw_star(self):
26 progress = update_progress()
27 if progress:
28 self.status_win.insch(0, self.width - 1, progress, curses.A_REVERSE)
29 self.status_win.refresh()
31 def _init_curses(self):
32 curses.use_default_colors()
33 curses.start_color()
34 curses.curs_set(0)
36 def dots(*positions):
37 return sum(map(lambda n: 1<<n, positions))
39 self.MATRIX_TO_CURSES = {
40 dots(): ' ',
41 dots(3, 4, 5): curses.ACS_HLINE,
42 dots(1, 4, 7): curses.ACS_VLINE,
43 dots(1, 3, 4, 5, 7): curses.ACS_PLUS,
44 dots(4, 5, 7): curses.ACS_ULCORNER,
45 dots(3, 4, 7): curses.ACS_URCORNER,
46 dots(1, 4, 5): curses.ACS_LLCORNER,
47 dots(1, 3, 4): curses.ACS_LRCORNER,
48 dots(3, 4, 5, 7): curses.ACS_TTEE,
49 dots(1, 3, 4, 5): curses.ACS_BTEE,
50 dots(1, 4, 5, 7): curses.ACS_LTEE,
51 dots(1, 3, 4, 7): curses.ACS_RTEE
54 try:
55 self.status_win = curses.newwin(0, 0)
56 self.panel = curses.newwin(0, 0)
57 self._resize()
58 except curses.error:
59 print 'Cannot create windows'
60 sys.exit(1)
62 def _resize(self):
63 """Called when the terminal size changes."""
64 self.height, self.width = self.window.getmaxyx()
65 self.height -= 1 # Status bar
66 self.need_tree = True
67 self.status_win.resize(1, self.width)
68 self.status_win.mvwin(self.height, 0)
69 self.panel.resize(self.height, self.width)
70 self._set_paths(self.paths)
71 self._refresh()
73 def _redraw(self):
74 self.window.refresh()
75 if self.need_tree:
76 self.tree = pysize_tree(self.paths, self.max_depth,
77 3.0 / self.height)
78 self.need_tree = False
79 if not self.tree.root:
80 return
81 self.panel.erase()
83 self._draw_matrix(CharMatrix(self.width, self.height, self.tree,
84 self.selected_node))
86 self.panel.refresh()
87 self.status_win.erase()
88 self.status_win.hline(0, 0, ord(' ') | curses.A_REVERSE, self.width)
89 status_line = sanitize_string(self.tree.root.get_name()) + ' '
90 status_line += human_unit(self.tree.root.size)
91 END = ' - Pysize'
92 if len(status_line) + len(END) >= self.width:
93 status_line = short_string(status_line, self.width - 2)
94 else:
95 status_line += END
96 self.status_win.addstr(status_line, curses.A_REVERSE)
97 self.status_win.refresh()
99 def _refresh(self):
100 while True:
101 try:
102 self._redraw()
103 break
104 except curses.error, e:
105 print e
106 time.sleep(1)
108 def _set_paths(self, paths):
109 self.selected_node = None
110 self.selection = None
111 self.paths = paths
112 self.need_tree = True
114 def _next_step(self):
115 self._set_paths([self.selected_node.get_dirname()])
117 def _select(self, function):
118 if not self.selected_node:
119 if self.tree.root.children:
120 self.selected_node = self.tree.root.children[0]
121 else:
122 # Forcing browsing to the left
123 self._set_paths([self.tree.root.get_dirname()])
124 return
125 selected = function(self.selected_node)
126 if not selected:
127 if function == self.tree.get_first_child and \
128 self.selected_node.is_dir():
129 # Browsing to the right
130 self._next_step()
131 return
132 self.selected_node = selected
133 if self.selected_node == self.tree.root:
134 # Browsing to the left
135 self._set_paths([self.tree.root.get_dirname()])
137 def _selected(self):
138 if self.selected_node:
139 self._set_paths(self.selected_node.get_fullpaths())
141 def run(self):
142 """The main dispatcher."""
143 key_bindings = {
144 ord('q'): lambda:
145 sys.exit(0),
147 curses.KEY_RESIZE: lambda:
148 self._resize(),
150 curses.KEY_DOWN: lambda:
151 self._select(self.tree.get_next_sibling),
153 curses.KEY_UP: lambda:
154 self._select(self.tree.get_previous_sibling),
156 curses.KEY_LEFT: lambda:
157 self._select(self.tree.get_parent),
159 curses.KEY_RIGHT: lambda:
160 self._select(self.tree.get_first_child),
162 ord('\n'): lambda:
163 self._selected()
166 while True:
167 self._refresh()
168 c = self.window.getch()
169 action = key_bindings.get(c, lambda: None)
170 action()
172 def _draw_matrix(self, matrix):
173 for (y, line) in enumerate(matrix.matrix):
174 for (x, char) in enumerate(line):
175 if isinstance(char, int):
176 selected = (char & SELECTED) != 0
177 char &= MASK
178 char = self.MATRIX_TO_CURSES[char]
179 if selected:
180 char |= curses.A_REVERSE
181 self.panel.insch(y, x, char)
182 else:
183 try:
184 char = char.encode('UTF-8')
185 except UnicodeDecodeError:
186 pass
187 self.panel.insstr(y, x, char)
189 def _run_curses(win, options, args):
190 app = _CursesApp(win, options, args)
191 app.run()
193 def run(options, args):
194 curses.wrapper(_run_curses, options, args)
196 def is_available():
197 return sys.stdin.isatty() and sys.stdout.isatty()