source: deluge/ui/console/screen.py@ cb14a2

2.0.x develop extjs4-port
Last change on this file since cb14a2 was cb14a2, checked in by Andrew Resch <andrewresch@gmail.com>, 16 years ago

Remove mapping.py

  • Property mode set to 100644
File size: 13.6 KB
Line 
1#
2# screen.py
3#
4# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
5#
6# Deluge is free software.
7#
8# You may redistribute it and/or modify it under the terms of the
9# GNU General Public License, as published by the Free Software
10# Foundation; either version 3 of the License, or (at your option)
11# any later version.
12#
13# deluge is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16# See the GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with deluge. If not, write to:
20# The Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor
22# Boston, MA 02110-1301, USA.
23#
24
25import curses
26import colors
27from deluge.log import LOG as log
28from twisted.internet import reactor
29
30class CursesStdIO(object):
31 """fake fd to be registered as a reader with the twisted reactor.
32 Curses classes needing input should extend this"""
33
34 def fileno(self):
35 """ We want to select on FD 0 """
36 return 0
37
38 def doRead(self):
39 """called when input is ready"""
40 pass
41 def logPrefix(self): return 'CursesClient'
42
43LINES_BUFFER_SIZE = 5000
44INPUT_HISTORY_SIZE = 500
45
46class Screen(CursesStdIO):
47 def __init__(self, stdscr, command_parser, tab_completer=None):
48 """
49 A curses screen designed to run as a reader in a twisted reactor.
50
51 :param command_parser: a function that will be passed a string when the
52 user hits enter
53 :param tab_completer: a function that is sent the `:prop:input` string when
54 the user hits tab. It's intended purpose is to modify the input string.
55 It should return a 2-tuple (input string, input cursor).
56
57 """
58 log.debug("Screen init!")
59 # Function to be called with commands
60 self.command_parser = command_parser
61 self.tab_completer = tab_completer
62 self.stdscr = stdscr
63 # Make the input calls non-blocking
64 self.stdscr.nodelay(1)
65
66 # Holds the user input and is cleared on 'enter'
67 self.input = ""
68 self.input_incomplete = ""
69 # Keep track of where the cursor is
70 self.input_cursor = 0
71 # Keep a history of inputs
72 self.input_history = []
73 self.input_history_index = 0
74
75 # Keep track of double-tabs
76 self.tab_count = 0
77
78 # Strings for the 2 status bars
79 self.topbar = ""
80 self.bottombar = ""
81
82 self.rows, self.cols = self.stdscr.getmaxyx()
83
84 # A list of strings to be displayed based on the offset (scroll)
85 self.lines = []
86 # The offset to display lines
87 self.display_lines_offset = 0
88
89 # Refresh the screen to display everything right away
90 self.refresh()
91
92 def connectionLost(self, reason):
93 self.close()
94
95 def add_line(self, text):
96 """
97 Add a line to the screen. This will be showed between the two bars.
98 The text can be formatted with color using the following format:
99
100 "{!fg, bg, attributes, ...!}"
101
102 See: http://docs.python.org/library/curses.html#constants for attributes.
103
104 Alternatively, it can use some built-in scheme for coloring.
105 See colors.py for built-in schemes.
106
107 "{!scheme!}"
108
109 Examples:
110
111 "{!blue, black, bold!}My Text is {!white, black!}cool"
112 "{!info!}I am some info text!"
113 "{!error!}Uh oh!"
114
115 :param text: str, the text to show
116 """
117
118 def get_line_chunks(line):
119 """
120 Returns a list of 2-tuples (color string, text)
121
122 """
123 chunks = []
124 num_chunks = line.count("{!")
125 for i in range(num_chunks):
126 # Find the beginning and end of the color tag
127 beg = line.find("{!")
128 end = line.find("!}") + 2
129 color = line[beg:end]
130 line = line[end:]
131
132 # Check to see if this is the last chunk
133 if i + 1 == num_chunks:
134 text = line
135 else:
136 # Not the last chunk so get the text up to the next tag
137 # and remove the text from line
138 text = line[:line.find("{!")]
139 line = line[line.find("{!"):]
140
141 chunks.append((color, text))
142
143 return chunks
144
145 for line in text.splitlines():
146 # We need to check for line lengths here and split as necessary
147 try:
148 line_length = colors.get_line_length(line)
149 except colors.BadColorString:
150 log.error("Passed a bad colored string..")
151 line_length = len(line)
152
153 if line_length >= (self.cols - 1):
154 s = ""
155 # The length of the text without the color tags
156 s_len = 0
157 # We need to split this over multiple lines
158 for chunk in get_line_chunks(line):
159 if (len(chunk[1]) + s_len) < (self.cols - 1):
160 # This chunk plus the current string in 's' isn't over
161 # the maximum width, so just append the color tag and text
162 s += chunk[0] + chunk[1]
163 s_len += len(chunk[1])
164 else:
165 # The chunk plus the current string in 's' is too long.
166 # We need to take as much of the chunk and put it into 's'
167 # with the color tag.
168 remain = (self.cols - 1) - s_len
169 s += chunk[0] + chunk[1][:remain]
170 # We append the line since it's full
171 self.lines.append(s)
172 # Start a new 's' with the remainder chunk
173 s = chunk[0] + chunk[1][remain:]
174 s_len = len(chunk[1][remain:])
175 # Append the final string which may or may not be the full width
176 if s:
177 self.lines.append(s)
178 else:
179 self.lines.append(line)
180
181 while len(self.lines) > LINES_BUFFER_SIZE:
182 # Remove the oldest line if the max buffer size has been reached
183 del self.lines[0]
184
185 self.refresh()
186
187 def add_string(self, row, string):
188 """
189 Adds a string to the desired `:param:row`.
190
191 :param row: int, the row number to write the string
192
193 """
194 col = 0
195 try:
196 parsed = colors.parse_color_string(string)
197 except colors.BadColorString, e:
198 log.error("Cannot add bad color string %s: %s", string, e)
199 return
200
201 for index, (color, s) in enumerate(parsed):
202 if index + 1 == len(parsed):
203 # This is the last string so lets append some " " to it
204 s += " " * (self.cols - (col + len(s)) - 1)
205 self.stdscr.addstr(row, col, s, color)
206 col += len(s)
207
208 def refresh(self):
209 """
210 Refreshes the screen.
211 Updates the lines based on the`:attr:lines` based on the `:attr:display_lines_offset`
212 attribute and the status bars.
213 """
214 self.stdscr.clear()
215
216 # Update the status bars
217 self.add_string(0, self.topbar)
218 self.add_string(self.rows - 2, self.bottombar)
219
220 # The number of rows minus the status bars and the input line
221 available_lines = self.rows - 3
222 # If the amount of lines exceeds the number of rows, we need to figure out
223 # which ones to display based on the offset
224 if len(self.lines) > available_lines:
225 # Get the lines to display based on the offset
226 offset = len(self.lines) - self.display_lines_offset
227 lines = self.lines[-(available_lines - offset):offset]
228 elif len(self.lines) == available_lines:
229 lines = self.lines
230 else:
231 lines = [""] * (available_lines - len(self.lines))
232 lines.extend(self.lines)
233
234 # Add the lines to the screen
235 for index, line in enumerate(lines):
236 self.add_string(index + 1, line)
237
238 # Add the input string
239 self.add_string(self.rows - 1, self.input)
240
241 # Move the cursor
242 self.stdscr.move(self.rows - 1, self.input_cursor)
243 self.stdscr.refresh()
244
245 def doRead(self):
246 """
247 Called when there is data to be read, ie, input from the keyboard.
248 """
249 # We wrap this function to catch exceptions and shutdown the mainloop
250 try:
251 self._doRead()
252 except Exception, e:
253 log.exception(e)
254 reactor.stop()
255
256 def _doRead(self):
257 # Read the character
258 c = self.stdscr.getch()
259
260 # We clear the input string and send it to the command parser on ENTER
261 if c == curses.KEY_ENTER or c == 10:
262 if self.input:
263 self.add_line(">>> " + self.input)
264 self.command_parser(self.input)
265 if len(self.input_history) == INPUT_HISTORY_SIZE:
266 # Remove the oldest input history if the max history size
267 # is reached.
268 del self.input_history[0]
269 self.input_history.append(self.input)
270 self.input_history_index = len(self.input_history)
271 self.input = ""
272 self.input_incomplete = ""
273 self.input_cursor = 0
274 self.stdscr.refresh()
275
276 # Run the tab completer function
277 elif c == 9:
278 # Keep track of tab hit count to know when it's double-hit
279 self.tab_count += 1
280 if self.tab_count > 1:
281 second_hit = True
282 self.tab_count = 0
283 else:
284 second_hit = False
285
286 if self.tab_completer:
287 # We only call the tab completer function if we're at the end of
288 # the input string on the cursor is on a space
289 if self.input_cursor == len(self.input) or self.input[self.input_cursor] == " ":
290 self.input, self.input_cursor = self.tab_completer(self.input, self.input_cursor, second_hit)
291
292 # We use the UP and DOWN keys to cycle through input history
293 elif c == curses.KEY_UP:
294 if self.input_history_index - 1 >= 0:
295 if self.input_history_index == len(self.input_history):
296 # We're moving from non-complete input so save it just incase
297 # we move back down to it.
298 self.input_incomplete = self.input
299 # Going back in the history
300 self.input_history_index -= 1
301 self.input = self.input_history[self.input_history_index]
302 self.input_cursor = len(self.input)
303 elif c == curses.KEY_DOWN:
304 if self.input_history_index + 1 < len(self.input_history):
305 # Going forward in the history
306 self.input_history_index += 1
307 self.input = self.input_history[self.input_history_index]
308 self.input_cursor = len(self.input)
309 elif self.input_history_index + 1 == len(self.input_history):
310 # We're moving back down to an incomplete input
311 self.input_history_index += 1
312 self.input = self.input_incomplete
313 self.input_cursor = len(self.input)
314
315 # Cursor movement
316 elif c == curses.KEY_LEFT:
317 if self.input_cursor:
318 self.input_cursor -= 1
319 elif c == curses.KEY_RIGHT:
320 if self.input_cursor < len(self.input):
321 self.input_cursor += 1
322 elif c == curses.KEY_HOME:
323 self.input_cursor = 0
324 elif c == curses.KEY_END:
325 self.input_cursor = len(self.input)
326
327 # Scrolling through buffer
328 elif c == curses.KEY_PPAGE:
329 self.display_lines_offset += self.rows - 3
330 # We substract 3 for the unavailable lines and 1 extra due to len(self.lines)
331 if self.display_lines_offset > (len(self.lines) - 4 - self.rows):
332 self.display_lines_offset = len(self.lines) - 4 - self.rows
333
334 self.refresh()
335 elif c == curses.KEY_NPAGE:
336 self.display_lines_offset -= self.rows - 3
337 if self.display_lines_offset < 0:
338 self.display_lines_offset = 0
339 self.refresh()
340
341 # Delete a character in the input string based on cursor position
342 if c == curses.KEY_BACKSPACE or c == 127:
343 if self.input and self.input_cursor > 0:
344 self.input = self.input[:self.input_cursor - 1] + self.input[self.input_cursor:]
345 self.input_cursor -= 1
346
347 # A key to add to the input string
348 else:
349 if c > 31 and c < 127:
350 if self.input_cursor == len(self.input):
351 self.input += chr(c)
352 else:
353 # Insert into string
354 self.input = self.input[:self.input_cursor] + chr(c) + self.input[self.input_cursor:]
355 # Move the cursor forward
356 self.input_cursor += 1
357
358 # We remove the tab count if the key wasn't a tab
359 if c != 9:
360 self.tab_count = 0
361
362 # Update the input string on the screen
363 self.add_string(self.rows - 1, self.input)
364 self.stdscr.move(self.rows - 1, self.input_cursor)
365 self.stdscr.refresh()
366
367 def close(self):
368 """
369 Clean up the curses stuff on exit.
370 """
371 curses.nocbreak()
372 self.stdscr.keypad(0)
373 curses.echo()
374 curses.endwin()
Note: See TracBrowser for help on using the repository browser.