]> code.delx.au - offlineimap/blob - offlineimap/head/offlineimap/ui/Curses.py
/offlineimap/head: changeset 365
[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, signal, time
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 signal.signal(signal.SIGWINCH, s.resizehandler)
322 s.resizelock = Lock()
323 s.resizecount = 0
324
325 def resizehandler(s, signum, frame):
326 s.resizeterm()
327
328 def resizeterm(s, dosleep = 1):
329 if not s.resizelock.acquire(0):
330 s.resizecount += 1
331 return
332 signal.signal(signal.SIGWINCH, signal.SIG_IGN)
333 s.aflock.acquire()
334 s.c.lock()
335 s.resizecount += 1
336 while s.resizecount:
337 s.c.reset()
338 s.setupwindows()
339 s.resizecount -= 1
340 s.c.unlock()
341 s.aflock.release()
342 s.resizelock.release()
343 signal.signal(signal.SIGWINCH, s.resizehandler)
344 if dosleep:
345 time.sleep(1)
346 s.resizeterm(0)
347
348 def isusable(s):
349 # Not a terminal? Can't use curses.
350 if not sys.stdout.isatty() and sys.stdin.isatty():
351 return 0
352
353 # No TERM specified? Can't use curses.
354 try:
355 if not len(os.environ['TERM']):
356 return 0
357 except: return 0
358
359 # ncurses doesn't want to start? Can't use curses.
360 # This test is nasty because initscr() actually EXITS on error.
361 # grr.
362
363 pid = os.fork()
364 if pid:
365 # parent
366 return not os.WEXITSTATUS(os.waitpid(pid, 0)[1])
367 else:
368 # child
369 curses.initscr()
370 curses.endwin()
371 # If we didn't die by here, indicate success.
372 sys.exit(0)
373
374 def keypress(s, key):
375 if key > 255:
376 return
377
378 if chr(key) == 'q':
379 # Request to quit.
380 s.terminate()
381
382 try:
383 index = acctkeys.index(chr(key))
384 except ValueError:
385 # Key not a valid one: exit.
386 return
387
388 if index > len(s.hotkeys):
389 # Not in our list of valid hotkeys.
390 return
391
392 # Trying to end sleep somewhere.
393
394 s.getaccountframe(s.hotkeys[index]).syncnow()
395
396 def getpass(s, accountname, config, errmsg = None):
397 s.inputhandler.input_acquire()
398
399 # See comment on _msg for info on why both locks are obtained.
400
401 s.tflock.acquire()
402 s.c.lock()
403 try:
404 s.gettf().setcolor('white')
405 s._addline(" *** Input Required", s.gettf().getcolorpair())
406 s._addline(" *** Please enter password for account %s: " % accountname,
407 s.gettf().getcolorpair())
408 s.logwindow.refresh()
409 password = s.logwindow.getstr()
410 finally:
411 s.tflock.release()
412 s.c.unlock()
413 s.inputhandler.input_release()
414 return password
415
416 def setupwindows(s):
417 s.c.lock()
418 try:
419 s.bannerwindow = curses.newwin(1, s.c.width, 0, 0)
420 s.setupwindow_drawbanner()
421 s.logheight = s.c.height - 1 - len(s.af.keys())
422 s.logwindow = curses.newwin(s.logheight, s.c.width, 1, 0)
423 s.logwindow.idlok(1)
424 s.logwindow.scrollok(1)
425 s.logwindow.move(s.logheight - 1, 0)
426 s.setupwindow_drawlog()
427 accounts = s.af.keys()
428 accounts.sort()
429 accounts.reverse()
430
431 pos = s.c.height - 1
432 index = 0
433 s.hotkeys = []
434 for account in accounts:
435 accountwindow = curses.newwin(1, s.c.width, pos, 0)
436 s.af[account].setwindow(accountwindow, acctkeys[index])
437 s.hotkeys.append(account)
438 index += 1
439 pos -= 1
440
441 curses.doupdate()
442 finally:
443 s.c.unlock()
444
445 def setupwindow_drawbanner(s):
446 if s.c.has_color:
447 color = s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLUE) | \
448 curses.A_BOLD
449 else:
450 color = curses.A_REVERSE
451 s.bannerwindow.bkgd(' ', color) # Fill background with that color
452 s.bannerwindow.addstr("%s %s" % (version.productname,
453 version.versionstr))
454 s.bannerwindow.addstr(0, s.bannerwindow.getmaxyx()[1] - len(version.copyright) - 1,
455 version.copyright)
456
457 s.bannerwindow.noutrefresh()
458
459 def setupwindow_drawlog(s):
460 if s.c.has_color:
461 color = s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLACK)
462 else:
463 color = curses.A_NORMAL
464 s.logwindow.bkgd(' ', color)
465 for line, color in s.text:
466 s.logwindow.addstr("\n" + line, color)
467 s.logwindow.noutrefresh()
468
469 def getaccountframe(s, accountname = None):
470 if accountname == None:
471 accountname = s.getthreadaccount()
472 s.aflock.acquire()
473 try:
474 if accountname in s.af:
475 return s.af[accountname]
476
477 # New one.
478 s.af[accountname] = CursesAccountFrame(s.c, accountname)
479 s.c.lock()
480 try:
481 s.c.reset()
482 s.setupwindows()
483 finally:
484 s.c.unlock()
485 finally:
486 s.aflock.release()
487 return s.af[accountname]
488
489
490 def _msg(s, msg, color = None):
491 if "\n" in msg:
492 for thisline in msg.split("\n"):
493 s._msg(thisline)
494 return
495
496 # We must acquire both locks. Otherwise, deadlock can result.
497 # This can happen if one thread calls _msg (locking curses, then
498 # tf) and another tries to set the color (locking tf, then curses)
499 #
500 # By locking both up-front here, in this order, we prevent deadlock.
501
502 s.tflock.acquire()
503 s.c.lock()
504 try:
505 if not s.c.isactive():
506 # For dumping out exceptions and stuff.
507 print msg
508 return
509 if color:
510 s.gettf().setcolor(color)
511 s._addline(msg, s.gettf().getcolorpair())
512 s.logwindow.refresh()
513 finally:
514 s.c.unlock()
515 s.tflock.release()
516
517 def _addline(s, msg, color):
518 s.c.lock()
519 try:
520 s.logwindow.addstr("\n" + msg, color)
521 s.text.append((msg, color))
522 while len(s.text) > s.logheight:
523 s.text = s.text[1:]
524 finally:
525 s.c.unlock()
526
527 def terminate(s, exitstatus = 0):
528 s.c.stop()
529 UIBase.terminate(s, exitstatus)
530
531 def threadException(s, thread):
532 s.c.stop()
533 UIBase.threadException(s, thread)
534
535 def mainException(s):
536 s.c.stop()
537 UIBase.mainException(s)
538
539 def sleep(s, sleepsecs):
540 s.gettf().setcolor('red')
541 s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
542 BlinkenBase.sleep(s, sleepsecs)
543
544 if __name__ == '__main__':
545 x = Blinkenlights(None)
546 x.init_banner()
547 import time
548 time.sleep(5)
549 x.c.stop()
550 fgs = {'black': curses.COLOR_BLACK, 'red': curses.COLOR_RED,
551 'green': curses.COLOR_GREEN, 'yellow': curses.COLOR_YELLOW,
552 'blue': curses.COLOR_BLUE, 'magenta': curses.COLOR_MAGENTA,
553 'cyan': curses.COLOR_CYAN, 'white': curses.COLOR_WHITE}
554
555 x = CursesUtil()
556 win1 = curses.newwin(x.height, x.width / 4 - 1, 0, 0)
557 win1.addstr("Black/normal\n")
558 for name, fg in fgs.items():
559 win1.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLACK))
560 win2 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1])
561 win2.addstr("Blue/normal\n")
562 for name, fg in fgs.items():
563 win2.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLUE))
564 win3 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1] +
565 win2.getmaxyx()[1])
566 win3.addstr("Black/bright\n")
567 for name, fg in fgs.items():
568 win3.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLACK) | \
569 curses.A_BOLD)
570 win4 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1] * 3)
571 win4.addstr("Blue/bright\n")
572 for name, fg in fgs.items():
573 win4.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLUE) | \
574 curses.A_BOLD)
575
576
577 win1.refresh()
578 win2.refresh()
579 win3.refresh()
580 win4.refresh()
581 x.stdscr.refresh()
582 import time
583 time.sleep(5)
584 x.stop()
585 print x.has_color
586 print x.height
587 print x.width
588