Add the possibility to put the name and the size on the same line in ASCII mode
[pysize.git] / pysize / ui / curses / ui_curses.py
blob9b92d889682327ea4b68d4737abc263180f2c3e1
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 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, short_string, 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.compute_size import size_observable
30 class _CursesApp(object):
31 """The curses UI."""
32 def __init__(self, win, options, args):
33 self.window = win
34 self.options = options
35 self.max_depth = options.max_depth
36 if options.min_size != 'auto':
37 raise Exception, 'curses UI supports only --min-size=auto'
38 self._set_paths(args)
39 self._init_curses()
40 self.last_star = 0
41 self.last_star_time = time.time()
42 size_observable.add_observer(self.draw_star)
44 def draw_star(self):
45 progress = update_progress()
46 if progress:
47 self.status_win.insch(0, self.width - 1, progress, curses.A_REVERSE)
48 self.status_win.refresh()
50 def _init_curses(self):
51 curses.use_default_colors()
52 curses.start_color()
53 curses.curs_set(0)
55 def dots(*positions):
56 return sum(map(lambda n: 1<<n, positions))
58 self.MATRIX_TO_CURSES = {
59 dots(): ' ',
60 dots(3, 4, 5): curses.ACS_HLINE,
61 dots(1, 4, 7): curses.ACS_VLINE,
62 dots(1, 3, 4, 5, 7): curses.ACS_PLUS,
63 dots(4, 5, 7): curses.ACS_ULCORNER,
64 dots(3, 4, 7): curses.ACS_URCORNER,
65 dots(1, 4, 5): curses.ACS_LLCORNER,
66 dots(1, 3, 4): curses.ACS_LRCORNER,
67 dots(3, 4, 5, 7): curses.ACS_TTEE,
68 dots(1, 3, 4, 5): curses.ACS_BTEE,
69 dots(1, 4, 5, 7): curses.ACS_LTEE,
70 dots(1, 3, 4, 7): curses.ACS_RTEE
73 try:
74 self.status_win = curses.newwin(0, 0)
75 self.panel = curses.newwin(0, 0)
76 self._resize()
77 except curses.error:
78 print 'Cannot create windows'
79 sys.exit(1)
81 def _resize(self):
82 """Called when the terminal size changes."""
83 self.height, self.width = self.window.getmaxyx()
84 self.height -= 1 # Status bar
85 self.need_tree = True
86 self.status_win.resize(1, self.width)
87 self.status_win.mvwin(self.height, 0)
88 self.panel.resize(self.height, self.width)
89 self._set_paths(self.paths)
90 self._refresh()
92 def _redraw(self):
93 self.window.refresh()
94 if self.need_tree:
95 # -1 accounts for the last horizontal line
96 self.tree = pysize_tree(self.paths, self.max_depth,
97 2.0 / (self.height - 1), self.options)
98 self.need_tree = False
99 if not self.tree.root:
100 return
101 self.panel.erase()
103 self._draw_matrix(CharMatrix(self.width, self.height, self.tree,
104 self.selected_node))
106 self.panel.refresh()
107 self.status_win.erase()
108 self.status_win.hline(0, 0, ord(' ') | curses.A_REVERSE, self.width)
109 status_line = sanitize_string(self.tree.root.get_name()) + ' '
110 status_line += human_unit(self.tree.root.size)
111 END = ' - Pysize'
112 if len(status_line) + len(END) >= self.width:
113 status_line = short_string(status_line, self.width - 2)
114 else:
115 status_line += END
116 self.status_win.addstr(status_line, curses.A_REVERSE)
117 self.status_win.refresh()
119 def _refresh(self):
120 while True:
121 try:
122 self._redraw()
123 break
124 except curses.error, e:
125 print e
126 time.sleep(1)
128 def _set_paths(self, paths):
129 self.selected_node = None
130 self.selection = None
131 self.paths = paths
132 self.need_tree = True
134 def _next_step(self):
135 self._set_paths([self.selected_node.get_dirname()])
137 def _select(self, function):
138 if not self.selected_node:
139 if self.tree.root.children:
140 self.selected_node = self.tree.root.children[0]
141 else:
142 # Forcing browsing to the left
143 self._set_paths([self.tree.root.get_dirname()])
144 return
145 selected = function(self.selected_node)
146 if not selected:
147 if function == self.tree.get_first_child and \
148 self.selected_node.is_dir():
149 # Browsing to the right
150 self._next_step()
151 return
152 self.selected_node = selected
153 if self.selected_node == self.tree.root:
154 # Browsing to the left
155 self._set_paths([self.tree.root.get_dirname()])
157 def _selected(self):
158 if self.selected_node:
159 self._set_paths(self.selected_node.get_fullpaths())
161 def run(self):
162 """The main dispatcher."""
163 key_bindings = {
164 ord('q'): lambda:
165 sys.exit(0),
167 curses.KEY_RESIZE: lambda:
168 self._resize(),
170 curses.KEY_DOWN: lambda:
171 self._select(self.tree.get_next_sibling),
173 curses.KEY_UP: lambda:
174 self._select(self.tree.get_previous_sibling),
176 curses.KEY_LEFT: lambda:
177 self._select(self.tree.get_parent),
179 curses.KEY_RIGHT: lambda:
180 self._select(self.tree.get_first_child),
182 ord('\n'): lambda:
183 self._selected()
186 while True:
187 self._refresh()
188 c = self.window.getch()
189 action = key_bindings.get(c, lambda: None)
190 action()
192 def _draw_matrix(self, matrix):
193 for y, line in enumerate(matrix.matrix):
194 for x, char in enumerate(line):
195 if isinstance(char, int):
196 selected = (char & SELECTED) != 0
197 char &= MASK
198 char = self.MATRIX_TO_CURSES[char]
199 if selected:
200 char |= curses.A_REVERSE
201 self.panel.insch(y, x, char)
202 else:
203 try:
204 char = char.encode('UTF-8')
205 except UnicodeDecodeError:
206 pass
207 self.panel.insstr(y, x, char)
209 def _run_curses(win, options, args):
210 args = args or ['.']
211 app = _CursesApp(win, options, args)
212 app.run()
214 def run(options, args):
215 if sys.stdin.isatty() and sys.stdout.isatty():
216 curses.wrapper(_run_curses, options, args)
217 else:
218 raise UINotAvailableException