]> code.delx.au - offlineimap/blob - offlineimap/ui/Curses.py
Daniel Jacobowitz patches
[offlineimap] / 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
19 from Blinkenlights import BlinkenBase
20 from UIBase import UIBase
21 from threading import *
22 import thread, time, sys, os, signal, time
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, ui):
130 s.c = master
131 s.children = []
132 s.accountname = accountname
133 s.ui = ui
134
135 def drawleadstr(s, secs = None):
136 if secs == None:
137 acctstr = '%s: [active] %13.13s: ' % (s.key, s.accountname)
138 else:
139 acctstr = '%s: [%3d:%02d] %13.13s: ' % (s.key,
140 secs / 60, secs % 60,
141 s.accountname)
142 s.c.locked(s.window.addstr, 0, 0, acctstr)
143 s.location = len(acctstr)
144
145 def setwindow(s, window, key):
146 s.window = window
147 s.key = key
148 s.drawleadstr()
149 for child in s.children:
150 child.update(window, 0, s.location)
151 s.location += 1
152
153 def getnewthreadframe(s):
154 tf = CursesThreadFrame(s.c, s.ui, s.window, 0, s.location)
155 s.location += 1
156 s.children.append(tf)
157 return tf
158
159 def startsleep(s, sleepsecs):
160 s.sleeping_abort = 0
161
162 def sleeping(s, sleepsecs, remainingsecs):
163 if remainingsecs:
164 s.c.lock()
165 try:
166 s.drawleadstr(remainingsecs)
167 s.window.refresh()
168 finally:
169 s.c.unlock()
170 time.sleep(sleepsecs)
171 else:
172 s.c.lock()
173 try:
174 s.drawleadstr()
175 s.window.refresh()
176 finally:
177 s.c.unlock()
178 return s.sleeping_abort
179
180 def syncnow(s):
181 s.sleeping_abort = 1
182
183 class CursesThreadFrame:
184 def __init__(s, master, ui, window, y, x):
185 """master should be a CursesUtil object."""
186 s.c = master
187 s.ui = ui
188 s.window = window
189 s.x = x
190 s.y = y
191 s.colors = []
192 bg = curses.COLOR_BLACK
193 s.colormap = {'black': s.c.getpair(curses.COLOR_BLACK, bg),
194 'gray': s.c.getpair(curses.COLOR_WHITE, bg),
195 'white': curses.A_BOLD | s.c.getpair(curses.COLOR_WHITE, bg),
196 'blue': s.c.getpair(curses.COLOR_BLUE, bg),
197 'red': s.c.getpair(curses.COLOR_RED, bg),
198 'purple': s.c.getpair(curses.COLOR_MAGENTA, bg),
199 'cyan': s.c.getpair(curses.COLOR_CYAN, bg),
200 'green': s.c.getpair(curses.COLOR_GREEN, bg),
201 'orange': s.c.getpair(curses.COLOR_YELLOW, bg),
202 'yellow': curses.A_BOLD | s.c.getpair(curses.COLOR_YELLOW, bg),
203 'pink': curses.A_BOLD | s.c.getpair(curses.COLOR_RED, bg)}
204 #s.setcolor('gray')
205 s.setcolor('black')
206
207 def setcolor(self, color):
208 self.color = self.colormap[color]
209 self.colorname = color
210 self.display()
211
212 def display(self):
213 def lockedstuff():
214 if self.getcolor() == 'black':
215 self.window.addstr(self.y, self.x, ' ', self.color)
216 else:
217 self.window.addstr(self.y, self.x, self.ui.config.getdefault("ui.Curses.Blinkenlights", "statuschar", '.'), self.color)
218 self.c.stdscr.move(self.c.height - 1, self.c.width - 1)
219 self.window.refresh()
220 self.c.locked(lockedstuff)
221
222 def getcolor(self):
223 return self.colorname
224
225 def getcolorpair(self):
226 return self.color
227
228 def update(self, window, y, x):
229 self.window = window
230 self.y = y
231 self.x = x
232 self.display()
233
234 def setthread(self, newthread):
235 self.setcolor('black')
236 #if newthread:
237 # self.setcolor('gray')
238 #else:
239 # self.setcolor('black')
240
241 class InputHandler:
242 def __init__(s, util):
243 s.c = util
244 s.bgchar = None
245 s.inputlock = Lock()
246 s.lockheld = 0
247 s.statuslock = Lock()
248 s.startup = Event()
249 s.startthread()
250
251 def startthread(s):
252 s.thread = threadutil.ExitNotifyThread(target = s.bgreaderloop,
253 name = "InputHandler loop")
254 s.thread.setDaemon(1)
255 s.thread.start()
256
257 def bgreaderloop(s):
258 while 1:
259 s.statuslock.acquire()
260 if s.lockheld or s.bgchar == None:
261 s.statuslock.release()
262 s.startup.wait()
263 else:
264 s.statuslock.release()
265 ch = s.c.stdscr.getch()
266 s.statuslock.acquire()
267 try:
268 if s.lockheld or s.bgchar == None:
269 curses.ungetch(ch)
270 else:
271 s.bgchar(ch)
272 finally:
273 s.statuslock.release()
274
275 def set_bgchar(s, callback):
276 """Sets a "background" character handler. If a key is pressed
277 while not doing anything else, it will be passed to this handler.
278
279 callback is a function taking a single arg -- the char pressed.
280
281 If callback is None, clears the request."""
282 s.statuslock.acquire()
283 oldhandler = s.bgchar
284 newhandler = callback
285 s.bgchar = callback
286
287 if oldhandler and not newhandler:
288 pass
289 if newhandler and not oldhandler:
290 s.startup.set()
291
292 s.statuslock.release()
293
294 def input_acquire(s):
295 """Call this method when you want exclusive input control.
296 Make sure to call input_release afterwards!
297 """
298
299 s.inputlock.acquire()
300 s.statuslock.acquire()
301 s.lockheld = 1
302 s.statuslock.release()
303
304 def input_release(s):
305 """Call this method when you are done getting input."""
306 s.statuslock.acquire()
307 s.lockheld = 0
308 s.statuslock.release()
309 s.inputlock.release()
310 s.startup.set()
311
312 class Blinkenlights(BlinkenBase, UIBase):
313 def init_banner(s):
314 s.af = {}
315 s.aflock = Lock()
316 s.c = CursesUtil()
317 s.text = []
318 BlinkenBase.init_banner(s)
319 s.setupwindows()
320 s.inputhandler = InputHandler(s.c)
321 s.gettf().setcolor('red')
322 s._msg(version.banner)
323 s.inputhandler.set_bgchar(s.keypress)
324 signal.signal(signal.SIGWINCH, s.resizehandler)
325 s.resizelock = Lock()
326 s.resizecount = 0
327
328 def resizehandler(s, signum, frame):
329 s.resizeterm()
330
331 def resizeterm(s, dosleep = 1):
332 if not s.resizelock.acquire(0):
333 s.resizecount += 1
334 return
335 signal.signal(signal.SIGWINCH, signal.SIG_IGN)
336 s.aflock.acquire()
337 s.c.lock()
338 s.resizecount += 1
339 while s.resizecount:
340 s.c.reset()
341 s.setupwindows()
342 s.resizecount -= 1
343 s.c.unlock()
344 s.aflock.release()
345 s.resizelock.release()
346 signal.signal(signal.SIGWINCH, s.resizehandler)
347 if dosleep:
348 time.sleep(1)
349 s.resizeterm(0)
350
351 def isusable(s):
352 # Not a terminal? Can't use curses.
353 if not sys.stdout.isatty() and sys.stdin.isatty():
354 return 0
355
356 # No TERM specified? Can't use curses.
357 try:
358 if not len(os.environ['TERM']):
359 return 0
360 except: return 0
361
362 # ncurses doesn't want to start? Can't use curses.
363 # This test is nasty because initscr() actually EXITS on error.
364 # grr.
365
366 pid = os.fork()
367 if pid:
368 # parent
369 return not os.WEXITSTATUS(os.waitpid(pid, 0)[1])
370 else:
371 # child
372 curses.initscr()
373 curses.endwin()
374 # If we didn't die by here, indicate success.
375 sys.exit(0)
376
377 def keypress(s, key):
378 if key > 255:
379 return
380
381 if chr(key) == 'q':
382 # Request to quit.
383 s.terminate()
384
385 try:
386 index = acctkeys.index(chr(key))
387 except ValueError:
388 # Key not a valid one: exit.
389 return
390
391 if index >= len(s.hotkeys):
392 # Not in our list of valid hotkeys.
393 return
394
395 # Trying to end sleep somewhere.
396
397 s.getaccountframe(s.hotkeys[index]).syncnow()
398
399 def getpass(s, accountname, config, errmsg = None):
400 s.inputhandler.input_acquire()
401
402 # See comment on _msg for info on why both locks are obtained.
403
404 s.tflock.acquire()
405 s.c.lock()
406 try:
407 s.gettf().setcolor('white')
408 s._addline(" *** Input Required", s.gettf().getcolorpair())
409 s._addline(" *** Please enter password for account %s: " % accountname,
410 s.gettf().getcolorpair())
411 s.logwindow.refresh()
412 password = s.logwindow.getstr()
413 finally:
414 s.tflock.release()
415 s.c.unlock()
416 s.inputhandler.input_release()
417 return password
418
419 def setupwindows(s):
420 s.c.lock()
421 try:
422 s.bannerwindow = curses.newwin(1, s.c.width, 0, 0)
423 s.setupwindow_drawbanner()
424 s.logheight = s.c.height - 1 - len(s.af.keys())
425 s.logwindow = curses.newwin(s.logheight, s.c.width, 1, 0)
426 s.logwindow.idlok(1)
427 s.logwindow.scrollok(1)
428 s.logwindow.move(s.logheight - 1, 0)
429 s.setupwindow_drawlog()
430 accounts = s.af.keys()
431 accounts.sort()
432 accounts.reverse()
433
434 pos = s.c.height - 1
435 index = 0
436 s.hotkeys = []
437 for account in accounts:
438 accountwindow = curses.newwin(1, s.c.width, pos, 0)
439 s.af[account].setwindow(accountwindow, acctkeys[index])
440 s.hotkeys.append(account)
441 index += 1
442 pos -= 1
443
444 curses.doupdate()
445 finally:
446 s.c.unlock()
447
448 def setupwindow_drawbanner(s):
449 if s.c.has_color:
450 color = s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLUE) | \
451 curses.A_BOLD
452 else:
453 color = curses.A_REVERSE
454 s.bannerwindow.bkgd(' ', color) # Fill background with that color
455 s.bannerwindow.addstr("%s %s" % (version.productname,
456 version.versionstr))
457 s.bannerwindow.addstr(0, s.bannerwindow.getmaxyx()[1] - len(version.copyright) - 1,
458 version.copyright)
459
460 s.bannerwindow.noutrefresh()
461
462 def setupwindow_drawlog(s):
463 if s.c.has_color:
464 color = s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLACK)
465 else:
466 color = curses.A_NORMAL
467 s.logwindow.bkgd(' ', color)
468 for line, color in s.text:
469 s.logwindow.addstr("\n" + line, color)
470 s.logwindow.noutrefresh()
471
472 def getaccountframe(s, accountname = None):
473 if accountname == None:
474 accountname = s.getthreadaccount()
475 s.aflock.acquire()
476 try:
477 if accountname in s.af:
478 return s.af[accountname]
479
480 # New one.
481 s.af[accountname] = CursesAccountFrame(s.c, accountname, s)
482 s.c.lock()
483 try:
484 s.c.reset()
485 s.setupwindows()
486 finally:
487 s.c.unlock()
488 finally:
489 s.aflock.release()
490 return s.af[accountname]
491
492
493 def _display(s, msg, color = None):
494 if "\n" in msg:
495 for thisline in msg.split("\n"):
496 s._msg(thisline)
497 return
498
499 # We must acquire both locks. Otherwise, deadlock can result.
500 # This can happen if one thread calls _msg (locking curses, then
501 # tf) and another tries to set the color (locking tf, then curses)
502 #
503 # By locking both up-front here, in this order, we prevent deadlock.
504
505 s.tflock.acquire()
506 s.c.lock()
507 try:
508 if not s.c.isactive():
509 # For dumping out exceptions and stuff.
510 print msg
511 return
512 if color:
513 s.gettf().setcolor(color)
514 elif s.gettf().getcolor() == 'black':
515 s.gettf().setcolor('gray')
516 s._addline(msg, s.gettf().getcolorpair())
517 s.logwindow.refresh()
518 finally:
519 s.c.unlock()
520 s.tflock.release()
521
522 def _addline(s, msg, color):
523 s.c.lock()
524 try:
525 s.logwindow.addstr("\n" + msg, color)
526 s.text.append((msg, color))
527 while len(s.text) > s.logheight:
528 s.text = s.text[1:]
529 finally:
530 s.c.unlock()
531
532 def terminate(s, exitstatus = 0, errortitle = None, errormsg = None):
533 s.c.stop()
534 UIBase.terminate(s, exitstatus = exitstatus, errortitle = errortitle, errormsg = errormsg)
535
536 def threadException(s, thread):
537 s.c.stop()
538 UIBase.threadException(s, thread)
539
540 def mainException(s):
541 s.c.stop()
542 UIBase.mainException(s)
543
544 def sleep(s, sleepsecs):
545 s.gettf().setcolor('red')
546 s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
547 BlinkenBase.sleep(s, sleepsecs)
548
549 if __name__ == '__main__':
550 x = Blinkenlights(None)
551 x.init_banner()
552 import time
553 time.sleep(5)
554 x.c.stop()
555 fgs = {'black': curses.COLOR_BLACK, 'red': curses.COLOR_RED,
556 'green': curses.COLOR_GREEN, 'yellow': curses.COLOR_YELLOW,
557 'blue': curses.COLOR_BLUE, 'magenta': curses.COLOR_MAGENTA,
558 'cyan': curses.COLOR_CYAN, 'white': curses.COLOR_WHITE}
559
560 x = CursesUtil()
561 win1 = curses.newwin(x.height, x.width / 4 - 1, 0, 0)
562 win1.addstr("Black/normal\n")
563 for name, fg in fgs.items():
564 win1.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLACK))
565 win2 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1])
566 win2.addstr("Blue/normal\n")
567 for name, fg in fgs.items():
568 win2.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLUE))
569 win3 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1] +
570 win2.getmaxyx()[1])
571 win3.addstr("Black/bright\n")
572 for name, fg in fgs.items():
573 win3.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLACK) | \
574 curses.A_BOLD)
575 win4 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1] * 3)
576 win4.addstr("Blue/bright\n")
577 for name, fg in fgs.items():
578 win4.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLUE) | \
579 curses.A_BOLD)
580
581
582 win1.refresh()
583 win2.refresh()
584 win3.refresh()
585 win4.refresh()
586 x.stdscr.refresh()
587 import time
588 time.sleep(5)
589 x.stop()
590 print x.has_color
591 print x.height
592 print x.width
593