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