Update copyright years
[pysize.git] / pysize / ui / curses / ui_curses.py
blob4b827e16ae0ff69741fb2bbdb19841ddd38911dc
1 # This program is free software; you can redistribute it and/or modify
2 # it under the terms of the GNU General Public License as published by
3 # the Free Software Foundation; either version 2 of the License, or
4 # (at your option) any later version.
6 # This program is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # GNU Library General Public License for more details.
11 # You should have received a copy of the GNU General Public License
12 # along with this program; if not, write to the Free Software
13 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
15 # See the COPYING file for license information.
17 # Copyright (c) 2006, 2007, 2008 Guillaume Chazarain <guichaz@yahoo.fr>
19 import curses
20 import os
21 import sys
22 import time
24 from pysize.ui.utils import human_unit, update_progress
25 from pysize.ui.utils import sanitize_string, UINotAvailableException
26 from pysize.ui.char_matrix import CharMatrix, MASK, SELECTED
27 from pysize.core.pysize_fs_tree import pysize_tree
28 from pysize.core.pysize_fs_node import pretty_paths
29 from pysize.core.compute_size import size_observable
30 from pysize.core.signals import install_sigquit_handler
32 class _CursesApp(object):
33 """The curses UI."""
34 def __init__(self, win, options, args):
35 self.window = win
36 self.options = options
37 self.max_depth = options.max_depth
38 self.tree = None
39 size_observable.add_observer(self.draw_star)
40 self._set_paths(args)
41 self._init_curses()
42 self.last_star = 0
43 self.last_star_time = time.time()
45 def draw_star(self):
46 progress = update_progress()
47 if progress:
48 self.status_win.insch(0, self.width - 1, progress, curses.A_REVERSE)
49 self.status_win.refresh()
51 def _init_curses(self):
52 curses.use_default_colors()
53 curses.start_color()
54 try:
55 curses.curs_set(0)
56 except curses.error:
57 # This call can fail with misconfigured terminals, for example
58 # TERM=xterm-color. This is harmless
59 pass
61 def dots(*positions):
62 return sum(map(lambda n: 1<<n, positions))
64 self.MATRIX_TO_CURSES = {
65 dots(): ' ',
66 dots(3, 4, 5): curses.ACS_HLINE,
67 dots(1, 4, 7): curses.ACS_VLINE,
68 dots(1, 3, 4, 5, 7): curses.ACS_PLUS,
69 dots(4, 5, 7): curses.ACS_ULCORNER,
70 dots(3, 4, 7): curses.ACS_URCORNER,
71 dots(1, 4, 5): curses.ACS_LLCORNER,
72 dots(1, 3, 4): curses.ACS_LRCORNER,
73 dots(3, 4, 5, 7): curses.ACS_TTEE,
74 dots(1, 3, 4, 5): curses.ACS_BTEE,
75 dots(1, 4, 5, 7): curses.ACS_LTEE,
76 dots(1, 3, 4, 7): curses.ACS_RTEE
79 try:
80 self.status_win = curses.newwin(0, 0)
81 self.panel = curses.newwin(0, 0)
82 self._resize()
83 except curses.error:
84 print 'Cannot create windows'
85 sys.exit(1)
87 def _resize(self):
88 """Called when the terminal size changes."""
89 self.height, self.width = self.window.getmaxyx()
90 self.height -= 1 # Status bar
91 self.need_tree = True
92 self.status_win.resize(1, self.width)
93 self.status_win.mvwin(self.height, 0)
94 self.panel.resize(self.height, self.width)
95 self._set_paths(self.paths)
96 self._refresh()
98 def _redraw(self):
99 self.window.refresh()
100 self.status_win.erase()
101 self.status_win.hline(0, 0, ord(' ') | curses.A_REVERSE, self.width)
102 if not self.tree:
103 status_line = '[exploring ' + pretty_paths(self.paths) + ']'
104 else:
105 status_line = self.tree.root.get_name() + ' '
106 status_line += human_unit(self.tree.root.size)
107 END = ' - Pysize'
108 if len(status_line) + len(END) > self.width:
109 status_line = sanitize_string(status_line,
110 max_length=self.width)
111 else:
112 status_line = sanitize_string(status_line + END)
114 self.status_win.insstr(status_line.encode('UTF-8'), curses.A_REVERSE)
115 self.status_win.refresh()
116 self.panel.erase()
117 if self.need_tree:
118 # -1 accounts for the last horizontal line
119 self.tree = pysize_tree(self.paths, self.max_depth,
120 2.0 / (self.height - 1), self.options)
121 self.need_tree = False
122 if not self.tree.root:
123 return
125 self._draw_matrix(CharMatrix(self.width, self.height, self.tree,
126 self.selected_node))
128 self.panel.refresh()
130 def _refresh(self):
131 while True:
132 try:
133 self._redraw()
134 break
135 except curses.error, e:
136 print e
137 time.sleep(1)
139 def _set_paths(self, paths):
140 self.selected_node = None
141 self.selection = None
142 self.paths = paths
143 self.need_tree = True
145 def _next_step(self):
146 self._set_paths([self.selected_node.get_dirname()])
148 def _select(self, function):
149 if not self.selected_node:
150 if self.tree.root.children:
151 self.selected_node = self.tree.root.children[0]
152 else:
153 # Forcing browsing to the left
154 self._set_paths([self.tree.root.get_dirname()])
155 return
156 selected = function(self.selected_node)
157 if not selected:
158 if function == self.tree.get_first_child and \
159 self.selected_node.is_dir():
160 # Browsing to the right
161 self._next_step()
162 return
163 self.selected_node = selected
164 if self.selected_node == self.tree.root:
165 # Browsing to the left
166 self._set_paths([self.tree.root.get_dirname()])
168 def _selected(self):
169 if self.selected_node:
170 self._set_paths(self.selected_node.get_fullpaths())
172 def run(self):
173 """The main dispatcher."""
174 key_bindings = {
175 ord('q'): lambda:
176 sys.exit(0),
178 curses.KEY_RESIZE: lambda:
179 self._resize(),
181 curses.KEY_DOWN: lambda:
182 self._select(self.tree.get_next_sibling),
184 curses.KEY_UP: lambda:
185 self._select(self.tree.get_previous_sibling),
187 curses.KEY_LEFT: lambda:
188 self._select(self.tree.get_parent),
190 curses.KEY_RIGHT: lambda:
191 self._select(self.tree.get_first_child),
193 ord('\n'): lambda:
194 self._selected()
197 while True:
198 self._refresh()
199 c = self.window.getch()
200 action = key_bindings.get(c, lambda: None)
201 action()
203 def _draw_matrix(self, matrix):
204 for y, line in enumerate(matrix.matrix):
205 for x, char in enumerate(line):
206 if isinstance(char, int):
207 selected = (char & SELECTED) != 0
208 char &= MASK
209 char = self.MATRIX_TO_CURSES[char]
210 if selected:
211 char |= curses.A_REVERSE
212 self.panel.insch(y, x, char)
213 else:
214 try:
215 char = char.encode('UTF-8')
216 except UnicodeDecodeError:
217 pass
218 self.panel.insstr(y, x, char)
220 def _run_curses(win, options, args):
221 install_sigquit_handler()
222 try:
223 args = args or [os.getcwd()]
224 app = _CursesApp(win, options, args)
225 app.run()
226 except KeyboardInterrupt:
227 sys.exit(0)
229 def run(options, args):
230 if sys.stdin.isatty() and sys.stdout.isatty():
231 curses.wrapper(_run_curses, options, args)
232 else:
233 raise UINotAvailableException