blob: 3c866c3ce9f7bc492da7a89702c5a90e0d3a2239 [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
68
69 def add_child(self, window):
70 self.children.append(window)
71 window.parent = self
72
73 def remove_child(self, window):
74 self.children.remove(window)
75
76 def set_first_responder(self, window):
77 if window.can_become_first_responder:
78 if callable(getattr(window, "hidden", None)) and window.hidden():
79 return False
80 if not window in self.children:
81 self.add_child(window)
82 self.first_responder = window
83 return True
84 else:
85 return False
86
87 def resign_first_responder(self, remove_from_parent, new_first_responder):
88 success = False
89 if self.parent:
90 if self.is_first_responder():
91 self.parent.first_responder = None
92 success = True
93 if remove_from_parent:
94 self.parent.remove_child(self)
95 if new_first_responder:
96 self.parent.set_first_responder(new_first_responder)
97 else:
98 self.parent.select_next_first_responder()
99 return success
Greg Clayton1827fc22015-09-19 00:39:09 +0000100
Greg Clayton87349242015-09-22 00:35:20 +0000101 def is_first_responder(self):
102 if self.parent:
103 return self.parent.first_responder == self
104 else:
105 return False
106
107 def select_next_first_responder(self):
108 num_children = len(self.children)
109 if num_children == 1:
110 return self.set_first_responder(self.children[0])
111 for (i,window) in enumerate(self.children):
112 if window.is_first_responder():
113 break
114 if i < num_children:
115 for i in range(i+1,num_children):
116 if self.set_first_responder(self.children[i]):
117 return True
118 for i in range(0, i):
119 if self.set_first_responder(self.children[i]):
120 return True
121
Greg Clayton1827fc22015-09-19 00:39:09 +0000122 def point_in_window(self, pt):
123 size = self.get_size()
124 return pt.x >= 0 and pt.x < size.w and pt.y >= 0 and pt.y < size.h
125
126 def addstr(self, pt, str):
127 try:
128 self.window.addstr(pt.y, pt.x, str)
129 except:
130 pass
131
132 def addnstr(self, pt, str, n):
133 try:
134 self.window.addnstr(pt.y, pt.x, str, n)
135 except:
136 pass
137
Greg Clayton87349242015-09-22 00:35:20 +0000138 def attron(self, attr):
139 return self.window.attron (attr)
140
141 def attroff(self, attr):
142 return self.window.attroff (attr)
143
144 def box(self, vertch=0, horch=0):
145 if vertch == 0:
146 vertch = curses.ACS_VLINE
147 if horch == 0:
148 horch = curses.ACS_HLINE
149 self.window.box(vertch, horch)
Greg Clayton1827fc22015-09-19 00:39:09 +0000150
151 def get_contained_rect(self, top_inset=0, bottom_inset=0, left_inset=0, right_inset=0, height=-1, width=-1):
152 '''Get a rectangle based on the top "height" lines of this window'''
153 rect = self.get_frame()
154 x = rect.origin.x + left_inset
155 y = rect.origin.y + top_inset
156 if height == -1:
157 h = rect.size.h - (top_inset + bottom_inset)
158 else:
159 h = height
160 if width == -1:
161 w = rect.size.w - (left_inset + right_inset)
162 else:
163 w = width
164 return Rect (x = x, y = y, w = w, h = h)
165
166 def erase(self):
167 self.window.erase()
168
169 def get_frame(self):
170 position = self.get_position()
171 size = self.get_size()
172 return Rect(x=position.x, y=position.y, w=size.w, h=size.h)
173
174 def get_position(self):
175 (y, x) = self.window.getbegyx()
176 return Point(x, y)
177
178 def get_size(self):
179 (y, x) = self.window.getmaxyx()
180 return Size(w=x, h=y)
181
182 def refresh(self):
Greg Clayton87349242015-09-22 00:35:20 +0000183 self.update()
Greg Clayton1827fc22015-09-19 00:39:09 +0000184 curses.panel.update_panels()
185 return self.window.refresh()
186
187 def resize(self, size):
Greg Clayton87349242015-09-22 00:35:20 +0000188 return self.window.resize(size.h, size.w)
Greg Clayton1827fc22015-09-19 00:39:09 +0000189
Greg Clayton87349242015-09-22 00:35:20 +0000190 def timeout(self, timeout_msec):
191 return self.window.timeout(timeout_msec)
192
193 def handle_key(self, key, check_parent=True):
194 '''Handle a key press in this window.'''
195
196 # First try the first responder if this window has one, but don't allow
197 # it to check with its parent (False second parameter) so we don't recurse
198 # and get a stack overflow
199 if self.first_responder:
200 if self.first_responder.handle_key(key, False):
201 return True
202
203 # Check if the window delegate wants to handle this key press
204 if self.delegate:
205 if callable(getattr(self.delegate, "handle_key", None)):
206 if self.delegate.handle_key(self, key):
207 return True
208 if self.delegate(self, key):
209 return True
210 # Check if we have a parent window and if so, let the parent
211 # window handle the key press
212 if check_parent and self.parent:
213 return self.parent.handle_key(key, True)
214 else:
215 return False # Key not handled
216
217 def update(self):
218 for child in self.children:
219 child.update()
220
221 def key_event_loop(self, timeout_msec=-1, n=sys.maxint):
222 '''Run an event loop to receive key presses and pass them along to the
223 responder chain.
224
225 timeout_msec is the timeout it milliseconds. If the value is -1, an
226 infinite wait will be used. It the value is zero, a non-blocking mode
227 will be used, and if greater than zero it will wait for a key press
228 for timeout_msec milliseconds.
229
230 n is the number of times to go through the event loop before exiting'''
231 self.timeout(timeout_msec)
232 while n > 0:
233 c = self.window.getch()
234 if c != -1:
235 self.handle_key(c)
236 n -= 1
237
Greg Clayton1827fc22015-09-19 00:39:09 +0000238class Panel(Window):
Greg Clayton87349242015-09-22 00:35:20 +0000239 def __init__(self, frame, delegate = None, can_become_first_responder = True):
Greg Clayton1827fc22015-09-19 00:39:09 +0000240 window = curses.newwin(frame.size.h,frame.size.w, frame.origin.y, frame.origin.x)
Greg Clayton87349242015-09-22 00:35:20 +0000241 super(Panel, self).__init__(window, delegate, can_become_first_responder)
Greg Clayton1827fc22015-09-19 00:39:09 +0000242 self.panel = curses.panel.new_panel(window)
243
Greg Clayton87349242015-09-22 00:35:20 +0000244 def hide(self):
245 return self.panel.hide()
246
247 def hidden(self):
248 return self.panel.hidden()
249
250 def show(self):
251 return self.panel.show()
252
Greg Clayton1827fc22015-09-19 00:39:09 +0000253 def top(self):
Greg Clayton87349242015-09-22 00:35:20 +0000254 return self.panel.top()
Greg Clayton1827fc22015-09-19 00:39:09 +0000255
256 def set_position(self, pt):
257 self.panel.move(pt.y, pt.x)
258
259 def slide_position(self, pt):
260 new_position = self.get_position()
261 new_position.x = new_position.x + pt.x
262 new_position.y = new_position.y + pt.y
263 self.set_position(new_position)
264
265class BoxedPanel(Panel):
Greg Clayton87349242015-09-22 00:35:20 +0000266 def __init__(self, frame, title, delegate = None, can_become_first_responder = True):
267 super(BoxedPanel, self).__init__(frame, delegate, can_become_first_responder)
Greg Clayton1827fc22015-09-19 00:39:09 +0000268 self.title = title
269 self.lines = list()
270 self.first_visible_idx = 0
Greg Clayton87349242015-09-22 00:35:20 +0000271 self.selected_idx = -1
Greg Clayton1827fc22015-09-19 00:39:09 +0000272 self.update()
273
Greg Claytond13c4fb2015-09-22 17:18:15 +0000274 def clear(self, update=True):
275 self.lines = list()
276 self.first_visible_idx = 0
277 self.selected_idx = -1
278 if update:
279 self.update()
280
Greg Clayton1827fc22015-09-19 00:39:09 +0000281 def get_usable_width(self):
282 '''Valid usable width is 0 to (width - 3) since the left and right lines display the box around
283 this frame and we skip a leading space'''
284 w = self.get_size().w
285 if w > 3:
286 return w-3
287 else:
288 return 0
289
290 def get_usable_height(self):
291 '''Valid line indexes are 0 to (height - 2) since the top and bottom lines display the box around this frame.'''
292 h = self.get_size().h
293 if h > 2:
294 return h-2
295 else:
296 return 0
297
298 def get_point_for_line(self, global_line_idx):
299 '''Returns the point to use when displaying a line whose index is "line_idx"'''
300 line_idx = global_line_idx - self.first_visible_idx
301 num_lines = self.get_usable_height()
302 if line_idx < num_lines:
303 return Point(x=2, y=1+line_idx)
304 else:
305 return Point(x=-1, y=-1) # return an invalid coordinate if the line index isn't valid
306
307 def set_title (self, title, update=True):
308 self.title = title
309 if update:
310 self.update()
311
Greg Clayton87349242015-09-22 00:35:20 +0000312 def scroll_begin (self):
313 self.first_visible_idx = 0
314 if len(self.lines) > 0:
315 self.selected_idx = 0
316 else:
317 self.selected_idx = -1
318 self.update()
319
320 def scroll_end (self):
321 max_visible_lines = self.get_usable_height()
322 num_lines = len(self.lines)
323 if max_visible_lines > num_lines:
324 self.first_visible_idx = num_lines - max_visible_lines
325 else:
326 self.first_visible_idx = 0
327 self.selected_idx = num_lines-1
328 self.update()
329
330 def select_next (self):
331 self.selected_idx += 1
332 if self.selected_idx >= len(self.lines):
333 self.selected_idx = len(self.lines) - 1
334 self.update()
335
336 def select_prev (self):
337 self.selected_idx -= 1
338 if self.selected_idx < 0:
339 if len(self.lines) > 0:
340 self.selected_idx = 0
341 else:
342 self.selected_idx = -1
343 self.update()
344
345 def get_selected_idx(self):
346 return self.selected_idx
347
Greg Clayton1827fc22015-09-19 00:39:09 +0000348 def _adjust_first_visible_line(self):
349 num_lines = len(self.lines)
350 max_visible_lines = self.get_usable_height()
351 if (num_lines - self.first_visible_idx) > max_visible_lines:
352 self.first_visible_idx = num_lines - max_visible_lines
353
354 def append_line(self, s, update=True):
355 self.lines.append(s)
356 self._adjust_first_visible_line()
357 if update:
358 self.update()
359
360 def set_line(self, line_idx, s, update=True):
361 '''Sets a line "line_idx" within the boxed panel to be "s"'''
362 if line_idx < 0:
363 return
364 while line_idx >= len(self.lines):
365 self.lines.append('')
366 self.lines[line_idx] = s
367 self._adjust_first_visible_line()
368 if update:
369 self.update()
370
371 def update(self):
372 self.erase()
Greg Clayton87349242015-09-22 00:35:20 +0000373 is_first_responder = self.is_first_responder()
374 if is_first_responder:
375 self.attron (curses.A_REVERSE)
Greg Clayton1827fc22015-09-19 00:39:09 +0000376 self.box()
Greg Clayton87349242015-09-22 00:35:20 +0000377 if is_first_responder:
378 self.attroff (curses.A_REVERSE)
Greg Clayton1827fc22015-09-19 00:39:09 +0000379 if self.title:
380 self.addstr(Point(x=2, y=0), ' ' + self.title + ' ')
381 max_width = self.get_usable_width()
382 for line_idx in range(self.first_visible_idx, len(self.lines)):
383 pt = self.get_point_for_line(line_idx)
384 if pt.is_valid_coordinate():
Greg Clayton87349242015-09-22 00:35:20 +0000385 is_selected = line_idx == self.selected_idx
386 if is_selected:
387 self.attron (curses.A_REVERSE)
Greg Clayton1827fc22015-09-19 00:39:09 +0000388 self.addnstr(pt, self.lines[line_idx], max_width)
Greg Clayton87349242015-09-22 00:35:20 +0000389 if is_selected:
390 self.attroff (curses.A_REVERSE)
Greg Clayton1827fc22015-09-19 00:39:09 +0000391 else:
392 return
393
394class StatusPanel(Panel):
395 def __init__(self, frame):
Greg Clayton87349242015-09-22 00:35:20 +0000396 super(StatusPanel, self).__init__(frame, delegate=None, can_become_first_responder=False)
Greg Clayton1827fc22015-09-19 00:39:09 +0000397 self.status_items = list()
398 self.status_dicts = dict()
399 self.next_status_x = 1
400
401 def add_status_item(self, name, title, format, width, value, update=True):
402 status_item_dict = { 'name': name,
403 'title' : title,
404 'width' : width,
405 'format' : format,
406 'value' : value,
407 'x' : self.next_status_x }
408 index = len(self.status_items)
409 self.status_items.append(status_item_dict)
410 self.status_dicts[name] = index
411 self.next_status_x += width + 2;
412 if update:
413 self.update()
414
415 def increment_status(self, name, update=True):
416 if name in self.status_dicts:
417 status_item_idx = self.status_dicts[name]
418 status_item_dict = self.status_items[status_item_idx]
419 status_item_dict['value'] = status_item_dict['value'] + 1
420 if update:
421 self.update()
422
423 def update_status(self, name, value, update=True):
424 if name in self.status_dicts:
425 status_item_idx = self.status_dicts[name]
426 status_item_dict = self.status_items[status_item_idx]
427 status_item_dict['value'] = status_item_dict['format'] % (value)
428 if update:
429 self.update()
430 def update(self):
431 self.erase();
432 for status_item_dict in self.status_items:
433 self.addnstr(Point(x=status_item_dict['x'], y=0), '%s: %s' % (status_item_dict['title'], status_item_dict['value']), status_item_dict['width'])
434
435stdscr = None
436
437def intialize_curses():
438 global stdscr
439 stdscr = curses.initscr()
440 curses.noecho()
441 curses.cbreak()
442 stdscr.keypad(1)
443 try:
444 curses.start_color()
445 except:
446 pass
447 return Window(stdscr)
448
449def terminate_curses():
450 global stdscr
451 if stdscr:
452 stdscr.keypad(0)
453 curses.echo()
454 curses.nocbreak()
455 curses.endwin()
456