Clean user given filenames, and set the same behaviour in curses as in gtk:
[pysize.git] / ui / curses / ui_curses.py
blob665b2014e2746bde615fac27379eff7b918e38bb
1 import curses
2 import os
3 import sys
4 import time
5 import locale
7 from ui.utils import human_unit, short_string
8 from ui.char_matrix import CharMatrix, MASK, SELECTED
9 from core.pysize_fs_tree import pysize_tree
11 class _CursesApp:
12 """The curses UI."""
13 def __init__(self, win, options, args):
14 self.window = win
15 self.max_depth = options.max_depth
16 if options.min_size != 'auto':
17 raise Exception, 'curses UI supports only --min-size=auto'
18 self._set_path(args[0])
19 self._init_curses()
21 def _init_curses(self):
22 curses.use_default_colors()
23 curses.start_color()
24 curses.curs_set(0)
26 def dots(*positions):
27 return sum(map(lambda n: 1<<n, positions))
29 self.MATRIX_TO_CURSES = {
30 dots(): ' ',
31 dots(3, 4, 5): curses.ACS_HLINE,
32 dots(1, 4, 7): curses.ACS_VLINE,
33 dots(1, 3, 4, 5, 7): curses.ACS_PLUS,
34 dots(4, 5, 7): curses.ACS_ULCORNER,
35 dots(3, 4, 7): curses.ACS_URCORNER,
36 dots(1, 4, 5): curses.ACS_LLCORNER,
37 dots(1, 3, 4): curses.ACS_LRCORNER,
38 dots(3, 4, 5, 7): curses.ACS_TTEE,
39 dots(1, 3, 4, 5): curses.ACS_BTEE,
40 dots(1, 4, 5, 7): curses.ACS_LTEE,
41 dots(1, 3, 4, 7): curses.ACS_RTEE
44 try:
45 self.status_win = curses.newwin(0, 0)
46 self.panel = curses.newwin(0, 0)
47 self._resize()
48 except curses.error:
49 print 'Cannot create windows'
50 sys.exit(1)
52 def _resize(self):
53 """Called when the terminal size changes."""
54 self.height, self.width = self.window.getmaxyx()
55 self.height -= 1 # Status bar
56 self.need_tree = True
57 self.status_win.resize(1, self.width)
58 self.status_win.mvwin(self.height, 0)
59 self.panel.resize(self.height, self.width)
60 self._set_path(self.path)
61 self._refresh()
63 def _redraw(self):
64 self.window.refresh()
65 if self.need_tree:
66 self.tree = pysize_tree(self.path, self.max_depth,
67 3.0 / self.height)
68 self.need_tree = False
69 if not self.tree.root:
70 return
71 self.panel.erase()
73 self._draw_matrix(CharMatrix(self.width, self.height, self.tree,
74 self.selected_node))
76 self.panel.refresh()
77 self.status_win.erase()
78 self.status_win.hline(0, 0, ord(' ') | curses.A_REVERSE, self.width)
79 END = ' - Pysize'
80 status_line = self.path + ' ' + human_unit(self.tree.root.size)
81 if len(status_line) + len(END) >= self.width:
82 status_line = short_string(status_line, self.width - 1)
83 else:
84 status_line += END
85 self.status_win.addstr(status_line, curses.A_REVERSE)
86 self.status_win.refresh()
88 def _refresh(self):
89 while True:
90 try:
91 self._redraw()
92 break
93 except curses.error, e:
94 print e
95 time.sleep(1)
97 def _set_path(self, path):
98 self.selected_node = None
99 self.selection = None
100 self.path = path
101 self.need_tree = True
103 def _next_step(self):
104 sequel = self.selected_node.get_fullpath()[len(self.path) + 1:]
105 sequel = sequel.split('/', 1)[0]
106 self._set_path(os.path.join(self.path, sequel))
108 def _select(self, function):
109 if not self.selected_node:
110 if self.tree.root.get_children():
111 self.selected_node = self.tree.root.get_children()[0]
112 return
113 selected = function(self.selected_node)
114 if not selected:
115 if function == self.tree.get_first_child and \
116 self.selected_node.is_dir():
117 # Browsing to the right
118 self._next_step()
119 return
120 self.selected_node = selected
121 if self.selected_node == self.tree.root:
122 # Browsing to the left
123 if len(self.path) > 1:
124 parent = os.path.dirname(self.path)
125 self._set_path(parent)
126 else:
127 self.selected_node = None
129 def _selected(self):
130 if self.selected_node and self.selected_node.is_dir():
131 self._set_path(self.selected_node.get_fullpath())
133 def run(self):
134 """The main dispatcher."""
135 key_bindings = {
136 ord('q'): lambda:
137 sys.exit(0),
139 curses.KEY_RESIZE: lambda:
140 self._resize(),
142 curses.KEY_DOWN: lambda:
143 self._select(self.tree.get_next_sibling),
145 curses.KEY_UP: lambda:
146 self._select(self.tree.get_previous_sibling),
148 curses.KEY_LEFT: lambda:
149 self._select(self.tree.get_parent),
151 curses.KEY_RIGHT: lambda:
152 self._select(self.tree.get_first_child),
154 ord('\n'): lambda:
155 self._selected()
158 while True:
159 self._refresh()
160 c = self.window.getch()
161 action = key_bindings.get(c, lambda: None)
162 action()
164 def _draw_matrix(self, matrix):
165 for (y, line) in enumerate(matrix.matrix):
166 for (x, char) in enumerate(line):
167 if isinstance(char, int):
168 selected = (char & SELECTED) != 0
169 char &= MASK
170 char = self.MATRIX_TO_CURSES[char]
171 if selected:
172 char |= curses.A_REVERSE
173 self.panel.insch(y, x, char)
174 else:
175 try:
176 char = char.encode('UTF-8')
177 except UnicodeDecodeError:
178 pass
179 self.panel.insstr(y, x, char)
181 def _run_curses(win, options, args):
182 app = _CursesApp(win, options, args)
183 app.run()
185 def run(options, args):
186 locale.setlocale(locale.LC_ALL, '')
187 curses.wrapper(_run_curses, options, args)
189 def is_available():
190 return sys.stdin.isatty() and sys.stdout.isatty()