]> code.delx.au - offlineimap/blob - offlineimap/head/offlineimap/ui/Curses.py
360787b256b25ed0bc11c835e8f497bcc7caa58b
[offlineimap] / offlineimap / head / offlineimap / ui / Curses.py
1 # Curses-based interfaces
2 # Copyright (C) 2003 John Goerzen
3 # <jgoerzen@complete.org>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; version 2 of the License.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
18 from Blinkenlights import BlinkenBase
19 from UIBase import UIBase
20 from threading import *
21 import thread, time, sys, os
22 from offlineimap import version, threadutil
23 from offlineimap.threadutil import MultiLock
24
25 import curses, curses.panel, curses.textpad, curses.wrapper
26
27 acctkeys = '1234567890abcdefghijklmnoprstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-=;/.,'
28
29 class CursesUtil:
30 def __init__(self):
31 self.pairlock = Lock()
32 self.iolock = MultiLock()
33 self.start()
34
35 def initpairs(self):
36 self.pairlock.acquire()
37 try:
38 self.pairs = {self._getpairindex(curses.COLOR_WHITE,
39 curses.COLOR_BLACK): 0}
40 self.nextpair = 1
41 finally:
42 self.pairlock.release()
43
44 def lock(self):
45 self.iolock.acquire()
46
47 def unlock(self):
48 self.iolock.release()
49
50 def locked(self, target, *args, **kwargs):
51 """Perform an operation with full locking."""
52 self.lock()
53 try:
54 apply(target, args, kwargs)
55 finally:
56 self.unlock()
57
58 def refresh(self):
59 def lockedstuff():
60 curses.panel.update_panels()
61 curses.doupdate()
62 self.locked(lockedstuff)
63
64 def isactive(self):
65 return hasattr(self, 'stdscr')
66
67 def _getpairindex(self, fg, bg):
68 return '%d/%d' % (fg,bg)
69
70 def getpair(self, fg, bg):
71 if not self.has_color:
72 return 0
73 pindex = self._getpairindex(fg, bg)
74 self.pairlock.acquire()
75 try:
76 if self.pairs.has_key(pindex):
77 return curses.color_pair(self.pairs[pindex])
78 else:
79 self.pairs[pindex] = self.nextpair
80 curses.init_pair(self.nextpair, fg, bg)
81 self.nextpair += 1
82 return curses.color_pair(self.nextpair - 1)
83 finally:
84 self.pairlock.release()
85
86 def start(self):
87 self.stdscr = curses.initscr()
88 curses.noecho()
89 curses.cbreak()
90 self.stdscr.keypad(1)
91 try:
92 curses.start_color()
93 self.has_color = curses.has_colors()
94 except:
95 self.has_color = 0
96
97 self.oldcursor = None
98 try:
99 self.oldcursor = curses.curs_set(0)
100 except:
101 pass
102
103 self.stdscr.clear()
104 self.stdscr.refresh()
105 (self.height, self.width) = self.stdscr.getmaxyx()
106 self.initpairs()
107
108 def stop(self):
109 if not hasattr(self, 'stdscr'):
110 return
111 #self.stdscr.addstr(self.height - 1, 0, "\n",
112 # self.getpair(curses.COLOR_WHITE,
113 # curses.COLOR_BLACK))
114 if self.oldcursor != None:
115 curses.curs_set(self.oldcursor)
116 self.stdscr.refresh()
117 self.stdscr.keypad(0)
118 curses.nocbreak()
119 curses.echo()
120 curses.endwin()
121 del self.stdscr
122
123 def reset(self):
124 self.stop()
125 self.start()
126
127 class CursesAccountFrame:
128 def __init__(s, master, accountname):
129 s.c = master
130 s.children = []
131 s.accountname = accountname
132
133 def drawleadstr(s, secs = None):
134 if secs == None:
135 acctstr = '%s: [active] %13.13s: ' % (s.key, s.accountname)
136 else:
137 acctstr = '%s: [%3d:%02d] %13.13s: ' % (s.key,
138 secs / 60, secs % 60,
139 s.accountname)
140 s.c.locked(s.window.addstr, 0, 0, acctstr)
141 s.location = len(acctstr)
142
143 def setwindow(s, window, key):
144 s.window = window
145 s.key = key
146 s.drawleadstr()
147 for child in s.children:
148 child.update(window, 0, s.location)
149 s.location += 1
150
151 def getnewthreadframe(s):
152 tf = CursesThreadFrame(s.c, s.window, 0, s.location)
153 s.location += 1
154 s.children.append(tf)
155 return tf
156
157 def startsleep(s, sleepsecs):
158 s.sleeping_abort = 0
159
160 def sleeping(s, sleepsecs, remainingsecs):
161 if remainingsecs:
162 s.c.lock()
163 try:
164 s.drawleadstr(remainingsecs)
165 s.window.refresh()
166 finally:
167 s.c.unlock()
168 time.sleep(sleepsecs)
169 else:
170 s.c.lock()
171 try:
172 s.drawleadstr()
173 s.window.refresh()
174 finally:
175 s.c.unlock()
176 return s.sleeping_abort
177
178 def syncnow(s):
179 s.sleeping_abort = 1
180
181 class CursesThreadFrame:
182 def __init__(s, master, window, y, x):
183 """master should be a CursesUtil object."""
184 s.c = master
185 s.window = window
186 s.x = x
187 s.y = y
188 s.colors = []
189 bg = curses.COLOR_BLACK
190 s.colormap = {'black': s.c.getpair(curses.COLOR_BLACK, bg),
191 'gray': s.c.getpair(curses.COLOR_WHITE, bg),
192 'white': curses.A_BOLD | s.c.getpair(curses.COLOR_WHITE, bg),
193 'blue': s.c.getpair(curses.COLOR_BLUE, bg),
194 'red': s.c.getpair(curses.COLOR_RED, bg),
195 'purple': s.c.getpair(curses.COLOR_MAGENTA, bg),
196 'cyan': s.c.getpair(curses.COLOR_CYAN, bg),
197 'green': s.c.getpair(curses.COLOR_GREEN, bg),
198 'orange': s.c.getpair(curses.COLOR_YELLOW, bg),
199 'yellow': curses.A_BOLD | s.c.getpair(curses.COLOR_YELLOW, bg),
200 'pink': curses.A_BOLD | s.c.getpair(curses.COLOR_RED, bg)}
201 #s.setcolor('gray')
202 s.setcolor('black')
203
204 def setcolor(self, color):
205 self.color = self.colormap[color]
206 self.colorname = color
207 self.display()
208
209 def display(self):
210 def lockedstuff():
211 if self.getcolor() == 'black':
212 self.window.addstr(self.y, self.x, ' ', self.color)
213 else:
214 self.window.addstr(self.y, self.x, '.', self.color)
215 self.c.stdscr.move(self.c.height - 1, self.c.width - 1)
216 self.window.refresh()
217 self.c.locked(lockedstuff)
218
219 def getcolor(self):
220 return self.colorname
221
222 def getcolorpair(self):
223 return self.color
224
225 def update(self, window, y, x):
226 self.window = window
227 self.y = y
228 self.x = x
229 self.display()
230
231 def setthread(self, newthread):
232 self.setcolor('black')
233 #if newthread:
234 # self.setcolor('gray')
235 #else:
236 # self.setcolor('black')
237
238 class InputHandler:
239 def __init__(s, util):
240 s.c = util
241 s.bgchar = None
242 s.inputlock = Lock()
243 s.lockheld = 0
244 s.statuslock = Lock()
245 s.startup = Event()
246 s.startthread()
247
248 def startthread(s):
249 s.thread = threadutil.ExitNotifyThread(target = s.bgreaderloop,
250 name = "InputHandler loop")
251 s.thread.setDaemon(1)
252 s.thread.start()
253
254 def bgreaderloop(s):
255 while 1:
256 s.statuslock.acquire()
257 if s.lockheld or s.bgchar == None:
258 s.statuslock.release()
259 s.startup.wait()
260 else:
261 s.statuslock.release()
262 ch = s.c.stdscr.getch()
263 s.statuslock.acquire()
264 try:
265 if s.lockheld or s.bgchar == None:
266 curses.ungetch(ch)
267 else:
268 s.bgchar(ch)
269 finally:
270 s.statuslock.release()
271
272 def set_bgchar(s, callback):
273 """Sets a "background" character handler. If a key is pressed
274 while not doing anything else, it will be passed to this handler.
275
276 callback is a function taking a single arg -- the char pressed.
277
278 If callback is None, clears the request."""
279 s.statuslock.acquire()
280 oldhandler = s.bgchar
281 newhandler = callback
282 s.bgchar = callback
283
284 if oldhandler and not newhandler:
285 pass
286 if newhandler and not oldhandler:
287 s.startup.set()
288
289 s.statuslock.release()
290
291 def input_acquire(s):
292 """Call this method when you want exclusive input control.
293 Make sure to call input_release afterwards!
294 """
295
296 s.inputlock.acquire()
297 s.statuslock.acquire()
298 s.lockheld = 1
299 s.statuslock.release()
300
301 def input_release(s):
302 """Call this method when you are done getting input."""
303 s.statuslock.acquire()
304 s.lockheld = 0
305 s.statuslock.release()
306 s.inputlock.release()
307 s.startup.set()
308
309 class Blinkenlights(BlinkenBase, UIBase):
310 def init_banner(s):
311 s.af = {}
312 s.aflock = Lock()
313 s.c = CursesUtil()
314 s.text = []
315 BlinkenBase.init_banner(s)
316 s.setupwindows()
317 s.inputhandler = InputHandler(s.c)
318 s.gettf().setcolor('red')
319 s._msg(version.banner)
320 s.inputhandler.set_bgchar(s.keypress)
321
322 def isusable(s):
323 # Not a terminal? Can't use curses.
324 if not sys.stdout.isatty() and sys.stdin.isatty():
325 return 0
326
327 # No TERM specified? Can't use curses.
328 try:
329 if not len(os.environ['TERM']):
330 return 0
331 except: return 0
332
333 # ncurses doesn't want to start? Can't use curses.
334 # This test is nasty because initscr() actually EXITS on error.
335 # grr.
336
337 pid = os.fork()
338 if pid:
339 # parent
340 return not os.WEXITSTATUS(os.waitpid(pid, 0)[1])
341 else:
342 # child
343 curses.initscr()
344 curses.endwin()
345 # If we didn't die by here, indicate success.
346 sys.exit(0)
347
348 def keypress(s, key):
349 if key > 255:
350 return
351
352 if chr(key) == 'q':
353 # Request to quit.
354 s.terminate()
355
356 try:
357 index = acctkeys.index(chr(key))
358 except ValueError:
359 # Key not a valid one: exit.
360 return
361
362 if index > len(s.hotkeys):
363 # Not in our list of valid hotkeys.
364 return
365
366 # Trying to end sleep somewhere.
367
368 s.getaccountframe(s.hotkeys[index]).syncnow()
369
370 def getpass(s, accountname, config, errmsg = None):
371 s.inputhandler.input_acquire()
372
373 # See comment on _msg for info on why both locks are obtained.
374
375 s.tflock.acquire()
376 s.c.lock()
377 try:
378 s.gettf().setcolor('white')
379 s._addline(" *** Input Required", s.gettf().getcolorpair())
380 s._addline(" *** Please enter password for account %s: " % accountname,
381 s.gettf().getcolorpair())
382 s.logwindow.refresh()
383 password = s.logwindow.getstr()
384 finally:
385 s.tflock.release()
386 s.c.unlock()
387 s.inputhandler.input_release()
388 return password
389
390 def setupwindows(s):
391 s.c.lock()
392 try:
393 s.bannerwindow = curses.newwin(1, s.c.width, 0, 0)
394 s.setupwindow_drawbanner()
395 s.logheight = s.c.height - 1 - len(s.af.keys())
396 s.logwindow = curses.newwin(s.logheight, s.c.width, 1, 0)
397 s.logwindow.idlok(1)
398 s.logwindow.scrollok(1)
399 s.logwindow.move(s.logheight - 1, 0)
400 s.setupwindow_drawlog()
401 accounts = s.af.keys()
402 accounts.sort()
403 accounts.reverse()
404
405 pos = s.c.height - 1
406 index = 0
407 s.hotkeys = []
408 for account in accounts:
409 accountwindow = curses.newwin(1, s.c.width, pos, 0)
410 s.af[account].setwindow(accountwindow, acctkeys[index])
411 s.hotkeys.append(account)
412 index += 1
413 pos -= 1
414
415 curses.doupdate()
416 finally:
417 s.c.unlock()
418
419 def setupwindow_drawbanner(s):
420 if s.c.has_color:
421 color = s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLUE) | \
422 curses.A_BOLD
423 else:
424 color = curses.A_REVERSE
425 s.bannerwindow.bkgd(' ', color) # Fill background with that color
426 s.bannerwindow.addstr("%s %s" % (version.productname,
427 version.versionstr))
428 s.bannerwindow.addstr(0, s.bannerwindow.getmaxyx()[1] - len(version.copyright) - 1,
429 version.copyright)
430
431 s.bannerwindow.noutrefresh()
432
433 def setupwindow_drawlog(s):
434 if s.c.has_color:
435 color = s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLACK)
436 else:
437 color = curses.A_NORMAL
438 s.logwindow.bkgd(' ', color)
439 for line, color in s.text:
440 s.logwindow.addstr("\n" + line, color)
441 s.logwindow.noutrefresh()
442
443 def getaccountframe(s, accountname = None):
444 if accountname == None:
445 accountname = s.getthreadaccount()
446 s.aflock.acquire()
447 try:
448 if accountname in s.af:
449 return s.af[accountname]
450
451 # New one.
452 s.af[accountname] = CursesAccountFrame(s.c, accountname)
453 s.c.lock()
454 try:
455 s.c.reset()
456 s.setupwindows()
457 finally:
458 s.c.unlock()
459 finally:
460 s.aflock.release()
461 return s.af[accountname]
462
463
464 def _msg(s, msg, color = None):
465 if "\n" in msg:
466 for thisline in msg.split("\n"):
467 s._msg(thisline)
468 return
469
470 # We must acquire both locks. Otherwise, deadlock can result.
471 # This can happen if one thread calls _msg (locking curses, then
472 # tf) and another tries to set the color (locking tf, then curses)
473 #
474 # By locking both up-front here, in this order, we prevent deadlock.
475
476 s.tflock.acquire()
477 s.c.lock()
478 try:
479 if not s.c.isactive():
480 # For dumping out exceptions and stuff.
481 print msg
482 return
483 if color:
484 s.gettf().setcolor(color)
485 s._addline(msg, s.gettf().getcolorpair())
486 s.logwindow.refresh()
487 finally:
488 s.c.unlock()
489 s.tflock.release()
490
491 def _addline(s, msg, color):
492 s.c.lock()
493 try:
494 s.logwindow.addstr("\n" + msg, color)
495 s.text.append((msg, color))
496 while len(s.text) > s.logheight:
497 s.text = s.text[1:]
498 finally:
499 s.c.unlock()
500
501 def terminate(s, exitstatus = 0):
502 s.c.stop()
503 UIBase.terminate(s, exitstatus)
504
505 def threadException(s, thread):
506 s.c.stop()
507 UIBase.threadException(s, thread)
508
509 def mainException(s):
510 s.c.stop()
511 UIBase.mainException(s)
512
513 def sleep(s, sleepsecs):
514 s.gettf().setcolor('red')
515 s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
516 BlinkenBase.sleep(s, sleepsecs)
517
518 if __name__ == '__main__':
519 x = Blinkenlights(None)
520 x.init_banner()
521 import time
522 time.sleep(5)
523 x.c.stop()
524 fgs = {'black': curses.COLOR_BLACK, 'red': curses.COLOR_RED,
525 'green': curses.COLOR_GREEN, 'yellow': curses.COLOR_YELLOW,
526 'blue': curses.COLOR_BLUE, 'magenta': curses.COLOR_MAGENTA,
527 'cyan': curses.COLOR_CYAN, 'white': curses.COLOR_WHITE}
528
529 x = CursesUtil()
530 win1 = curses.newwin(x.height, x.width / 4 - 1, 0, 0)
531 win1.addstr("Black/normal\n")
532 for name, fg in fgs.items():
533 win1.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLACK))
534 win2 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1])
535 win2.addstr("Blue/normal\n")
536 for name, fg in fgs.items():
537 win2.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLUE))
538 win3 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1] +
539 win2.getmaxyx()[1])
540 win3.addstr("Black/bright\n")
541 for name, fg in fgs.items():
542 win3.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLACK) | \
543 curses.A_BOLD)
544 win4 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1] * 3)
545 win4.addstr("Blue/bright\n")
546 for name, fg in fgs.items():
547 win4.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLUE) | \
548 curses.A_BOLD)
549
550
551 win1.refresh()
552 win2.refresh()
553 win3.refresh()
554 win4.refresh()
555 x.stdscr.refresh()
556 import time
557 time.sleep(5)
558 x.stop()
559 print x.has_color
560 print x.height
561 print x.width
562