blob: 058ce6b8b4111fbda344fce7d86a0d76862627fe [file] [log] [blame]
Greg Clayton1827fc22015-09-19 00:39:09 +00001import curses, curses.panel
Greg Clayton87349242015-09-22 00:35:20 +00002import sys
3import time
Greg Clayton1827fc22015-09-19 00:39:09 +00004
5class Point(object):
6 def __init__(self, x, y):
7 self.x = x
8 self.y = y
9
10 def __repr__(self):
11 return str(self)
12
13 def __str__(self):
14 return "(x=%u, y=%u)" % (self.x, self.y)
15
16 def is_valid_coordinate(self):
17 return self.x >= 0 and self.y >= 0
18
19class Size(object):
20 def __init__(self, w, h):
21 self.w = w
22 self.h = h
23
24 def __repr__(self):
25 return str(self)
26
27 def __str__(self):
28 return "(w=%u, h=%u)" % (self.w, self.h)
29
30class Rect(object):
31 def __init__(self, x=0, y=0, w=0, h=0):
32 self.origin = Point(x, y)
33 self.size = Size(w, h)
34
35 def __repr__(self):
36 return str(self)
37
38 def __str__(self):
39 return "{ %s, %s }" % (str(self.origin), str(self.size))
40
41 def get_min_x(self):
42 return self.origin.x
43
44 def get_max_x(self):
45 return self.origin.x + self.size.w
46
47 def get_min_y(self):
48 return self.origin.y
49
50 def get_max_y(self):
51 return self.origin.y + self.size.h
52
53 def contains_point(self, pt):
54 if pt.x < self.get_max_x():
55 if pt.y < self.get_max_y():
56 if pt.x >= self.get_min_y():
57 return pt.y >= self.get_min_y()
58 return False
59
60class Window(object):
Greg Clayton87349242015-09-22 00:35:20 +000061 def __init__(self, window, delegate = None, can_become_first_responder = True):
Greg Clayton1827fc22015-09-19 00:39:09 +000062 self.window = window
Greg Clayton87349242015-09-22 00:35:20 +000063 self.parent = None
64 self.delegate = delegate
65 self.children = list()
66 self.first_responder = None
67 self.can_become_first_responder = can_become_first_responder
Greg Clayton414dba52015-09-24 00:19:42 +000068 self.key_actions = dict()
Greg Clayton87349242015-09-22 00:35:20 +000069
70 def add_child(self, window):
71 self.children.append(window)
72 window.parent = self
Greg Clayton414dba52015-09-24 00:19:42 +000073
74 def add_key_action(self, arg, callback, decription):
75 if isinstance(arg, list):
76 for key in arg:
77 self.add_key_action(key, callback, description)
78 else:
79 if isinstance(arg, ( int, long )):
80 key_action_dict = { 'key' : arg,
81 'callback' : callback,
82 'description' : decription }
83 self.key_actions[arg] = key_action_dict
84 elif isinstance(arg, basestring):
85 key_integer = ord(arg)
86 key_action_dict = { 'key' : key_integer,
87 'callback' : callback,
88 'description' : decription }
89 self.key_actions[key_integer] = key_action_dict
90 else:
91 raise ValueError
92
Greg Clayton87349242015-09-22 00:35:20 +000093 def remove_child(self, window):
94 self.children.remove(window)
95
96 def set_first_responder(self, window):
97 if window.can_become_first_responder:
98 if callable(getattr(window, "hidden", None)) and window.hidden():
99 return False
100 if not window in self.children:
101 self.add_child(window)
102 self.first_responder = window
103 return True
104 else:
105 return False
106
107 def resign_first_responder(self, remove_from_parent, new_first_responder):
108 success = False
109 if self.parent:
110 if self.is_first_responder():
111 self.parent.first_responder = None
112 success = True
113 if remove_from_parent:
114 self.parent.remove_child(self)
115 if new_first_responder:
116 self.parent.set_first_responder(new_first_responder)
117 else:
118 self.parent.select_next_first_responder()
119 return success
Greg Clayton1827fc22015-09-19 00:39:09 +0000120
Greg Clayton87349242015-09-22 00:35:20 +0000121 def is_first_responder(self):
122 if self.parent:
123 return self.parent.first_responder == self
124 else:
125 return False
126
127 def select_next_first_responder(self):
128 num_children = len(self.children)
129 if num_children == 1:
130 return self.set_first_responder(self.children[0])
131 for (i,window) in enumerate(self.children):
132 if window.is_first_responder():
133 break
134 if i < num_children:
135 for i in range(i+1,num_children):
136 if self.set_first_responder(self.children[i]):
137 return True
138 for i in range(0, i):
139 if self.set_first_responder(self.children[i]):
140 return True
141
Greg Clayton1827fc22015-09-19 00:39:09 +0000142 def point_in_window(self, pt):
143 size = self.get_size()
144 return pt.x >= 0 and pt.x < size.w and pt.y >= 0 and pt.y < size.h
145
146 def addstr(self, pt, str):
147 try:
148 self.window.addstr(pt.y, pt.x, str)
149 except:
150 pass
151
152 def addnstr(self, pt, str, n):
153 try:
154 self.window.addnstr(pt.y, pt.x, str, n)
155 except:
156 pass
157
Greg Clayton87349242015-09-22 00:35:20 +0000158 def attron(self, attr):
159 return self.window.attron (attr)
160
161 def attroff(self, attr):
162 return self.window.attroff (attr)
163
164 def box(self, vertch=0, horch=0):
165 if vertch == 0:
166 vertch = curses.ACS_VLINE
167 if horch == 0:
168 horch = curses.ACS_HLINE
169 self.window.box(vertch, horch)
Greg Clayton1827fc22015-09-19 00:39:09 +0000170
171 def get_contained_rect(self, top_inset=0, bottom_inset=0, left_inset=0, right_inset=0, height=-1, width=-1):
172 '''Get a rectangle based on the top "height" lines of this window'''
173 rect = self.get_frame()
174 x = rect.origin.x + left_inset
175 y = rect.origin.y + top_inset
176 if height == -1:
177 h = rect.size.h - (top_inset + bottom_inset)
178 else:
179 h = height
180 if width == -1:
181 w = rect.size.w - (left_inset + right_inset)
182 else:
183 w = width
184 return Rect (x = x, y = y, w = w, h = h)
185
186 def erase(self):
187 self.window.erase()
188
189 def get_frame(self):
190 position = self.get_position()
191 size = self.get_size()
192 return Rect(x=position.x, y=position.y, w=size.w, h=size.h)
193
194 def get_position(self):
195 (y, x) = self.window.getbegyx()
196 return Point(x, y)
197
198 def get_size(self):
199 (y, x) = self.window.getmaxyx()
200 return Size(w=x, h=y)
201
202 def refresh(self):
Greg Clayton87349242015-09-22 00:35:20 +0000203 self.update()
Greg Clayton1827fc22015-09-19 00:39:09 +0000204 curses.panel.update_panels()
205 return self.window.refresh()
206
207 def resize(self, size):
Greg Clayton87349242015-09-22 00:35:20 +0000208 return self.window.resize(size.h, size.w)
Greg Clayton1827fc22015-09-19 00:39:09 +0000209
Greg Clayton87349242015-09-22 00:35:20 +0000210 def timeout(self, timeout_msec):
211 return self.window.timeout(timeout_msec)
212
213 def handle_key(self, key, check_parent=True):
214 '''Handle a key press in this window.'''
215
216 # First try the first responder if this window has one, but don't allow
217 # it to check with its parent (False second parameter) so we don't recurse
218 # and get a stack overflow
219 if self.first_responder:
220 if self.first_responder.handle_key(key, False):
221 return True
Greg Clayton414dba52015-09-24 00:19:42 +0000222
223 # Check our key map to see if we have any actions. Actions don't take
224 # any arguments, they must be callable
225 if key in self.key_actions:
226 key_action = self.key_actions[key]
227 key_action['callback']()
228 return True
229 # Check if there is a wildcard key for any key
230 if -1 in self.key_actions:
231 key_action = self.key_actions[-1]
232 key_action['callback']()
233 return True
Greg Clayton87349242015-09-22 00:35:20 +0000234 # Check if the window delegate wants to handle this key press
235 if self.delegate:
236 if callable(getattr(self.delegate, "handle_key", None)):
237 if self.delegate.handle_key(self, key):
238 return True
239 if self.delegate(self, key):
240 return True
241 # Check if we have a parent window and if so, let the parent
242 # window handle the key press
243 if check_parent and self.parent:
244 return self.parent.handle_key(key, True)
245 else:
246 return False # Key not handled
247
248 def update(self):
249 for child in self.children:
250 child.update()
251
252 def key_event_loop(self, timeout_msec=-1, n=sys.maxint):
253 '''Run an event loop to receive key presses and pass them along to the
254 responder chain.
255
256 timeout_msec is the timeout it milliseconds. If the value is -1, an
257 infinite wait will be used. It the value is zero, a non-blocking mode
258 will be used, and if greater than zero it will wait for a key press
259 for timeout_msec milliseconds.
260
261 n is the number of times to go through the event loop before exiting'''
262 self.timeout(timeout_msec)
263 while n > 0:
264 c = self.window.getch()
265 if c != -1:
266 self.handle_key(c)
267 n -= 1
268
Greg Clayton1827fc22015-09-19 00:39:09 +0000269class Panel(Window):
Greg Clayton87349242015-09-22 00:35:20 +0000270 def __init__(self, frame, delegate = None, can_become_first_responder = True):
Greg Clayton1827fc22015-09-19 00:39:09 +0000271 window = curses.newwin(frame.size.h,frame.size.w, frame.origin.y, frame.origin.x)
Greg Clayton87349242015-09-22 00:35:20 +0000272 super(Panel, self).__init__(window, delegate, can_become_first_responder)
Greg Clayton1827fc22015-09-19 00:39:09 +0000273 self.panel = curses.panel.new_panel(window)
274
Greg Clayton87349242015-09-22 00:35:20 +0000275 def hide(self):
276 return self.panel.hide()
277
278 def hidden(self):
279 return self.panel.hidden()
280
281 def show(self):
282 return self.panel.show()
283
Greg Clayton1827fc22015-09-19 00:39:09 +0000284 def top(self):
Greg Clayton87349242015-09-22 00:35:20 +0000285 return self.panel.top()
Greg Clayton1827fc22015-09-19 00:39:09 +0000286
287 def set_position(self, pt):
288 self.panel.move(pt.y, pt.x)
289
290 def slide_position(self, pt):
291 new_position = self.get_position()
292 new_position.x = new_position.x + pt.x
293 new_position.y = new_position.y + pt.y
294 self.set_position(new_position)
295
296class BoxedPanel(Panel):
Greg Clayton87349242015-09-22 00:35:20 +0000297 def __init__(self, frame, title, delegate = None, can_become_first_responder = True):
298 super(BoxedPanel, self).__init__(frame, delegate, can_become_first_responder)
Greg Clayton1827fc22015-09-19 00:39:09 +0000299 self.title = title
300 self.lines = list()
301 self.first_visible_idx = 0
Greg Clayton87349242015-09-22 00:35:20 +0000302 self.selected_idx = -1
Greg Clayton1827fc22015-09-19 00:39:09 +0000303 self.update()
304
Greg Claytond13c4fb2015-09-22 17:18:15 +0000305 def clear(self, update=True):
306 self.lines = list()
307 self.first_visible_idx = 0
308 self.selected_idx = -1
309 if update:
310 self.update()
311
Greg Clayton1827fc22015-09-19 00:39:09 +0000312 def get_usable_width(self):
313 '''Valid usable width is 0 to (width - 3) since the left and right lines display the box around
314 this frame and we skip a leading space'''
315 w = self.get_size().w
316 if w > 3:
317 return w-3
318 else:
319 return 0
320
321 def get_usable_height(self):
322 '''Valid line indexes are 0 to (height - 2) since the top and bottom lines display the box around this frame.'''
323 h = self.get_size().h
324 if h > 2:
325 return h-2
326 else:
327 return 0
328
329 def get_point_for_line(self, global_line_idx):
330 '''Returns the point to use when displaying a line whose index is "line_idx"'''
331 line_idx = global_line_idx - self.first_visible_idx
332 num_lines = self.get_usable_height()
333 if line_idx < num_lines:
334 return Point(x=2, y=1+line_idx)
335 else:
336 return Point(x=-1, y=-1) # return an invalid coordinate if the line index isn't valid
337
338 def set_title (self, title, update=True):
339 self.title = title
340 if update:
341 self.update()
342
Greg Clayton87349242015-09-22 00:35:20 +0000343 def scroll_begin (self):
344 self.first_visible_idx = 0
345 if len(self.lines) > 0:
346 self.selected_idx = 0
347 else:
348 self.selected_idx = -1
349 self.update()
350
351 def scroll_end (self):
352 max_visible_lines = self.get_usable_height()
353 num_lines = len(self.lines)
Greg Clayton414dba52015-09-24 00:19:42 +0000354 if num_lines > max_visible_lines:
Greg Clayton87349242015-09-22 00:35:20 +0000355 self.first_visible_idx = num_lines - max_visible_lines
356 else:
357 self.first_visible_idx = 0
358 self.selected_idx = num_lines-1
359 self.update()
360
361 def select_next (self):
362 self.selected_idx += 1
363 if self.selected_idx >= len(self.lines):
364 self.selected_idx = len(self.lines) - 1
365 self.update()
366
367 def select_prev (self):
368 self.selected_idx -= 1
369 if self.selected_idx < 0:
370 if len(self.lines) > 0:
371 self.selected_idx = 0
372 else:
373 self.selected_idx = -1
374 self.update()
375
376 def get_selected_idx(self):
377 return self.selected_idx
378
Greg Clayton1827fc22015-09-19 00:39:09 +0000379 def _adjust_first_visible_line(self):
380 num_lines = len(self.lines)
381 max_visible_lines = self.get_usable_height()
382 if (num_lines - self.first_visible_idx) > max_visible_lines:
383 self.first_visible_idx = num_lines - max_visible_lines
384
385 def append_line(self, s, update=True):
386 self.lines.append(s)
387 self._adjust_first_visible_line()
388 if update:
389 self.update()
390
391 def set_line(self, line_idx, s, update=True):
392 '''Sets a line "line_idx" within the boxed panel to be "s"'''
393 if line_idx < 0:
394 return
395 while line_idx >= len(self.lines):
396 self.lines.append('')
397 self.lines[line_idx] = s
398 self._adjust_first_visible_line()
399 if update:
400 self.update()
401
402 def update(self):
403 self.erase()
Greg Clayton87349242015-09-22 00:35:20 +0000404 is_first_responder = self.is_first_responder()
405 if is_first_responder:
406 self.attron (curses.A_REVERSE)
Greg Clayton1827fc22015-09-19 00:39:09 +0000407 self.box()
Greg Clayton87349242015-09-22 00:35:20 +0000408 if is_first_responder:
409 self.attroff (curses.A_REVERSE)
Greg Clayton1827fc22015-09-19 00:39:09 +0000410 if self.title:
411 self.addstr(Point(x=2, y=0), ' ' + self.title + ' ')
412 max_width = self.get_usable_width()
413 for line_idx in range(self.first_visible_idx, len(self.lines)):
414 pt = self.get_point_for_line(line_idx)
415 if pt.is_valid_coordinate():
Greg Clayton87349242015-09-22 00:35:20 +0000416 is_selected = line_idx == self.selected_idx
417 if is_selected:
418 self.attron (curses.A_REVERSE)
Greg Clayton1827fc22015-09-19 00:39:09 +0000419 self.addnstr(pt, self.lines[line_idx], max_width)
Greg Clayton87349242015-09-22 00:35:20 +0000420 if is_selected:
421 self.attroff (curses.A_REVERSE)
Greg Clayton1827fc22015-09-19 00:39:09 +0000422 else:
423 return
424
425class StatusPanel(Panel):
426 def __init__(self, frame):
Greg Clayton87349242015-09-22 00:35:20 +0000427 super(StatusPanel, self).__init__(frame, delegate=None, can_become_first_responder=False)
Greg Clayton1827fc22015-09-19 00:39:09 +0000428 self.status_items = list()
429 self.status_dicts = dict()
430 self.next_status_x = 1
431
432 def add_status_item(self, name, title, format, width, value, update=True):
433 status_item_dict = { 'name': name,
434 'title' : title,
435 'width' : width,
436 'format' : format,
437 'value' : value,
438 'x' : self.next_status_x }
439 index = len(self.status_items)
440 self.status_items.append(status_item_dict)
441 self.status_dicts[name] = index
442 self.next_status_x += width + 2;
443 if update:
444 self.update()
445
446 def increment_status(self, name, update=True):
447 if name in self.status_dicts:
448 status_item_idx = self.status_dicts[name]
449 status_item_dict = self.status_items[status_item_idx]
450 status_item_dict['value'] = status_item_dict['value'] + 1
451 if update:
452 self.update()
453
454 def update_status(self, name, value, update=True):
455 if name in self.status_dicts:
456 status_item_idx = self.status_dicts[name]
457 status_item_dict = self.status_items[status_item_idx]
458 status_item_dict['value'] = status_item_dict['format'] % (value)
459 if update:
460 self.update()
461 def update(self):
462 self.erase();
463 for status_item_dict in self.status_items:
464 self.addnstr(Point(x=status_item_dict['x'], y=0), '%s: %s' % (status_item_dict['title'], status_item_dict['value']), status_item_dict['width'])
465
466stdscr = None
467
468def intialize_curses():
469 global stdscr
470 stdscr = curses.initscr()
471 curses.noecho()
472 curses.cbreak()
473 stdscr.keypad(1)
474 try:
475 curses.start_color()
476 except:
477 pass
478 return Window(stdscr)
479
480def terminate_curses():
481 global stdscr
482 if stdscr:
483 stdscr.keypad(0)
484 curses.echo()
485 curses.nocbreak()
486 curses.endwin()
487