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