]> code.delx.au - offlineimap/blob - offlineimap/head/offlineimap/ui/Curses.py
/offlineimap/head: changeset 314
[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
23 from offlineimap import version, threadutil
24 from offlineimap.threadutil import MultiLock
25
26 import curses, curses.panel, curses.textpad, curses.wrapper
27 from debuglock import DebuggingLock
28
29 class CursesUtil:
30 def __init__(self):
31 self.pairs = {self._getpairindex(curses.COLOR_WHITE,
32 curses.COLOR_BLACK): 0}
33 self.start()
34 self.nextpair = 1
35 self.pairlock = Lock()
36 self.iolock = MultiLock()
37
38 def lock(self):
39 self.iolock.acquire()
40
41 def unlock(self):
42 self.iolock.release()
43
44 def locked(self, target, *args, **kwargs):
45 """Perform an operation with full locking."""
46 self.lock()
47 try:
48 apply(target, args, kwargs)
49 finally:
50 self.unlock()
51
52 def refresh(self):
53 def lockedstuff():
54 curses.panel.update_panels()
55 curses.doupdate()
56 self.locked(lockedstuff)
57
58 def isactive(self):
59 return hasattr(self, 'stdscr')
60
61 def _getpairindex(self, fg, bg):
62 return '%d/%d' % (fg,bg)
63
64 def getpair(self, fg, bg):
65 pindex = self._getpairindex(fg, bg)
66 self.pairlock.acquire()
67 try:
68 if self.pairs.has_key(pindex):
69 return curses.color_pair(self.pairs[pindex])
70 else:
71 self.pairs[pindex] = self.nextpair
72 curses.init_pair(self.nextpair, fg, bg)
73 self.nextpair += 1
74 return curses.color_pair(self.nextpair - 1)
75 finally:
76 self.pairlock.release()
77
78 def start(self):
79 self.stdscr = curses.initscr()
80 curses.noecho()
81 curses.cbreak()
82 self.stdscr.keypad(1)
83 try:
84 curses.start_color()
85 self.has_color = curses.has_colors()
86 except:
87 self.has_color = 0
88
89 self.stdscr.clear()
90 self.stdscr.refresh()
91 (self.height, self.width) = self.stdscr.getmaxyx()
92
93 def stop(self):
94 if not hasattr(self, 'stdscr'):
95 return
96 #self.stdscr.addstr(self.height - 1, 0, "\n",
97 # self.getpair(curses.COLOR_WHITE,
98 # curses.COLOR_BLACK))
99 self.stdscr.refresh()
100 self.stdscr.keypad(0)
101 curses.nocbreak()
102 curses.echo()
103 curses.endwin()
104 del self.stdscr
105
106 def reset(self):
107 self.stop()
108 self.start()
109
110 class CursesAccountFrame:
111 def __init__(s, master, accountname):
112 s.c = master
113 s.children = []
114 s.accountname = accountname
115
116 def setwindow(s, window):
117 s.window = window
118 acctstr = '%15.15s: ' % s.accountname
119 s.window.addstr(0, 0, acctstr)
120 s.location = len(acctstr)
121 for child in s.children:
122 child.update(window, 0, s.location)
123 s.location += 1
124
125 def getnewthreadframe(s):
126 tf = CursesThreadFrame(s.c, s.window, 0, s.location)
127 s.location += 1
128 s.children.append(tf)
129 return tf
130
131 class CursesThreadFrame:
132 def __init__(s, master, window, y, x):
133 """master should be a CursesUtil object."""
134 s.c = master
135 s.window = window
136 s.x = x
137 s.y = y
138 s.colors = []
139 bg = curses.COLOR_BLACK
140 s.colormap = {'black': s.c.getpair(curses.COLOR_BLACK, bg),
141 'gray': s.c.getpair(curses.COLOR_WHITE, bg),
142 'white': curses.A_BOLD | s.c.getpair(curses.COLOR_WHITE, bg),
143 'blue': s.c.getpair(curses.COLOR_BLUE, bg),
144 'red': s.c.getpair(curses.COLOR_RED, bg),
145 'purple': s.c.getpair(curses.COLOR_MAGENTA, bg),
146 'cyan': s.c.getpair(curses.COLOR_CYAN, bg),
147 'green': s.c.getpair(curses.COLOR_GREEN, bg),
148 'orange': s.c.getpair(curses.COLOR_YELLOW, bg),
149 'yellow': curses.A_BOLD | s.c.getpair(curses.COLOR_YELLOW, bg),
150 'pink': curses.A_BOLD | s.c.getpair(curses.COLOR_RED, bg)}
151 #s.setcolor('gray')
152 s.setcolor('black')
153
154 def setcolor(self, color):
155 self.color = self.colormap[color]
156 self.display()
157
158 def display(self):
159 def lockedstuff():
160 self.window.addstr(self.y, self.x, '.', self.color)
161 self.c.stdscr.move(self.c.height - 1, self.c.width - 1)
162 self.window.refresh()
163 self.c.locked(lockedstuff)
164
165 def getcolor(self):
166 return self.color
167
168 def update(self, window, y, x):
169 self.window = window
170 self.y = y
171 self.x = x
172 self.display()
173
174 def setthread(self, newthread):
175 self.setcolor('black')
176 #if newthread:
177 # self.setcolor('gray')
178 #else:
179 # self.setcolor('black')
180
181 class InputHandler:
182 def __init__(s, util):
183 s.c = util
184 s.bgchar = None
185 s.inputlock = DebuggingLock('inputlock')
186 s.lockheld = 0
187 s.statuslock = DebuggingLock('statuslock')
188 s.startup = Event()
189 s.startthread()
190
191 def startthread(s):
192 s.thread = threadutil.ExitNotifyThread(target = s.bgreaderloop,
193 name = "InputHandler loop")
194 s.thread.setDaemon(1)
195 s.thread.start()
196
197 def bgreaderloop(s):
198 while 1:
199 s.statuslock.acquire()
200 if s.lockheld or s.bgchar == None:
201 s.statuslock.release()
202 s.startup.wait()
203 else:
204 s.statuslock.release()
205 ch = s.c.stdscr.getch()
206 s.statuslock.acquire()
207 try:
208 if s.lockheld or s.bgchar == None:
209 curses.ungetch(ch)
210 else:
211 s.bgchar(ch)
212 finally:
213 s.statuslock.release()
214
215 def set_bgchar(s, callback):
216 """Sets a "background" character handler. If a key is pressed
217 while not doing anything else, it will be passed to this handler.
218
219 callback is a function taking a single arg -- the char pressed.
220
221 If callback is None, clears the request."""
222 s.statuslock.acquire()
223 oldhandler = s.bgchar
224 newhandler = callback
225 s.bgchar = callback
226
227 if oldhandler and not newhandler:
228 pass
229 if newhandler and not oldhandler:
230 s.startup.set()
231
232 s.statuslock.release()
233
234 def input_acquire(s):
235 """Call this method when you want exclusive input control.
236 Make sure to call input_release afterwards!
237 """
238
239 s.inputlock.acquire()
240 s.statuslock.acquire()
241 s.lockheld = 1
242 s.statuslock.release()
243
244 def input_release(s):
245 """Call this method when you are done getting input."""
246 s.statuslock.acquire()
247 s.lockheld = 0
248 s.statuslock.release()
249 s.inputlock.release()
250 s.startup.set()
251
252 class Blinkenlights(BlinkenBase, UIBase):
253 def init_banner(s):
254 s.af = {}
255 s.aflock = DebuggingLock('aflock')
256 s.c = CursesUtil()
257 s.text = []
258 BlinkenBase.init_banner(s)
259 s.setupwindows()
260 s.inputhandler = InputHandler(s.c)
261 s.gettf().setcolor('red')
262 s._msg(version.banner)
263 s.inputhandler.set_bgchar(s.keypress)
264
265 def keypress(s, key):
266 s._msg("Key pressed: " + str(key))
267
268 def getpass(s, accountname, config, errmsg = None):
269 s.inputhandler.input_acquire()
270
271 # See comment on _msg for info on why both locks are obtained.
272
273 s.tflock.acquire()
274 s.c.lock()
275 try:
276 s.gettf().setcolor('white')
277 s._addline(" *** Input Required", s.gettf().getcolor())
278 s._addline(" *** Please enter password for account %s: " % accountname,
279 s.gettf().getcolor())
280 s.logwindow.refresh()
281 password = s.logwindow.getstr()
282 finally:
283 s.tflock.release()
284 s.c.unlock()
285 s.inputhandler.input_release()
286 return password
287
288 def setupwindows(s):
289 s.c.lock()
290 try:
291 s.bannerwindow = curses.newwin(1, s.c.width, 0, 0)
292 s.setupwindow_drawbanner()
293 s.logheight = s.c.height - 1 - len(s.af.keys())
294 s.logwindow = curses.newwin(s.logheight, s.c.width, 1, 0)
295 s.logwindow.idlok(1)
296 s.logwindow.scrollok(1)
297 s.setupwindow_drawlog()
298 accounts = s.af.keys()
299 accounts.sort()
300 accounts.reverse()
301
302 pos = s.c.height - 1
303 for account in accounts:
304 accountwindow = curses.newwin(1, s.c.width, pos, 0)
305 s.af[account].setwindow(accountwindow)
306 pos -= 1
307
308 curses.doupdate()
309 finally:
310 s.c.unlock()
311
312 def setupwindow_drawbanner(s):
313 s.bannerwindow.bkgd(' ', curses.A_BOLD | \
314 s.c.getpair(curses.COLOR_WHITE,
315 curses.COLOR_BLUE))
316 s.bannerwindow.addstr("%s %s" % (version.productname,
317 version.versionstr))
318 s.bannerwindow.addstr(0, s.bannerwindow.getmaxyx()[1] - len(version.copyright) - 1,
319 version.copyright)
320
321 s.bannerwindow.noutrefresh()
322
323 def setupwindow_drawlog(s):
324 s.logwindow.bkgd(' ', s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLACK))
325 for line, color in s.text:
326 s.logwindow.addstr(line + "\n", color)
327 s.logwindow.noutrefresh()
328
329 def getaccountframe(s):
330 accountname = s.getthreadaccount()
331 s.aflock.acquire()
332 try:
333 if accountname in s.af:
334 return s.af[accountname]
335
336 # New one.
337 s.af[accountname] = CursesAccountFrame(s.c, accountname)
338 s.c.lock()
339 try:
340 s.c.reset()
341 s.setupwindows()
342 finally:
343 s.c.unlock()
344 finally:
345 s.aflock.release()
346 return s.af[accountname]
347
348
349 def _msg(s, msg, color = None):
350 if "\n" in msg:
351 for thisline in msg.split("\n"):
352 s._msg(thisline)
353 return
354
355 # We must acquire both locks. Otherwise, deadlock can result.
356 # This can happen if one thread calls _msg (locking curses, then
357 # tf) and another tries to set the color (locking tf, then curses)
358 #
359 # By locking both up-front here, in this order, we prevent deadlock.
360
361 s.tflock.acquire()
362 s.c.lock()
363 try:
364 if not s.c.isactive():
365 # For dumping out exceptions and stuff.
366 print msg
367 return
368 if color:
369 s.gettf().setcolor(color)
370 s._addline(msg, s.gettf().getcolor())
371 s.logwindow.refresh()
372 finally:
373 s.c.unlock()
374 s.tflock.release()
375
376 def _addline(s, msg, color):
377 s.c.lock()
378 try:
379 s.logwindow.addstr(msg + "\n", color)
380 s.text.append((msg, color))
381 while len(s.text) > s.logheight:
382 s.text = s.text[1:]
383 finally:
384 s.c.unlock()
385
386 def terminate(s, exitstatus = 0):
387 s.c.stop()
388 UIBase.terminate(s, exitstatus)
389
390 def threadException(s, thread):
391 s.c.stop()
392 UIBase.threadException(s, thread)
393
394 def mainException(s):
395 s.c.stop()
396 UIBase.mainException(s)
397
398 if __name__ == '__main__':
399 x = Blinkenlights(None)
400 x.init_banner()
401 import time
402 time.sleep(10)
403 x.c.stop()
404 fgs = {'black': curses.COLOR_BLACK, 'red': curses.COLOR_RED,
405 'green': curses.COLOR_GREEN, 'yellow': curses.COLOR_YELLOW,
406 'blue': curses.COLOR_BLUE, 'magenta': curses.COLOR_MAGENTA,
407 'cyan': curses.COLOR_CYAN, 'white': curses.COLOR_WHITE}
408
409 x = CursesUtil()
410 win1 = curses.newwin(x.height, x.width / 4 - 1, 0, 0)
411 win1.addstr("Black/normal\n")
412 for name, fg in fgs.items():
413 win1.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLACK))
414 win2 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1])
415 win2.addstr("Blue/normal\n")
416 for name, fg in fgs.items():
417 win2.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLUE))
418 win3 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1] +
419 win2.getmaxyx()[1])
420 win3.addstr("Black/bright\n")
421 for name, fg in fgs.items():
422 win3.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLACK) | \
423 curses.A_BOLD)
424 win4 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1] * 3)
425 win4.addstr("Blue/bright\n")
426 for name, fg in fgs.items():
427 win4.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLUE) | \
428 curses.A_BOLD)
429
430
431 win1.refresh()
432 win2.refresh()
433 win3.refresh()
434 win4.refresh()
435 x.stdscr.refresh()
436 import time
437 time.sleep(40)
438 x.stop()
439 print x.has_color
440 print x.height
441 print x.width
442