]>
code.delx.au - offlineimap/blob - offlineimap/ui/Curses.py
1 # Curses-based interfaces
2 # Copyright (C) 2003 John Goerzen
3 # <jgoerzen@complete.org>
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.
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.
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
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
26 import curses
, curses
.panel
, curses
.textpad
, curses
.wrapper
28 acctkeys
= '1234567890abcdefghijklmnoprstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-=;/.,'
32 self
.pairlock
= Lock()
33 self
.iolock
= MultiLock()
37 self
.pairlock
.acquire()
39 self
.pairs
= {self
._getpairindex
(curses
.COLOR_WHITE
,
40 curses
.COLOR_BLACK
): 0}
43 self
.pairlock
.release()
51 def locked(self
, target
, *args
, **kwargs
):
52 """Perform an operation with full locking."""
55 apply(target
, args
, kwargs
)
61 curses
.panel
.update_panels()
63 self
.locked(lockedstuff
)
66 return hasattr(self
, 'stdscr')
68 def _getpairindex(self
, fg
, bg
):
69 return '%d/%d' % (fg
,bg
)
71 def getpair(self
, fg
, bg
):
72 if not self
.has_color
:
74 pindex
= self
._getpairindex
(fg
, bg
)
75 self
.pairlock
.acquire()
77 if self
.pairs
.has_key(pindex
):
78 return curses
.color_pair(self
.pairs
[pindex
])
80 self
.pairs
[pindex
] = self
.nextpair
81 curses
.init_pair(self
.nextpair
, fg
, bg
)
83 return curses
.color_pair(self
.nextpair
- 1)
85 self
.pairlock
.release()
88 self
.stdscr
= curses
.initscr()
94 self
.has_color
= curses
.has_colors()
100 self
.oldcursor
= curses
.curs_set(0)
105 self
.stdscr
.refresh()
106 (self
.height
, self
.width
) = self
.stdscr
.getmaxyx()
110 if not hasattr(self
, 'stdscr'):
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)
128 class CursesAccountFrame
:
129 def __init__(s
, master
, accountname
, ui
):
132 s
.accountname
= accountname
135 def drawleadstr(s
, secs
= None):
137 acctstr
= '%s: [active] %13.13s: ' % (s
.key
, s
.accountname
)
139 acctstr
= '%s: [%3d:%02d] %13.13s: ' % (s
.key
,
140 secs
/ 60, secs
% 60,
142 s
.c
.locked(s
.window
.addstr
, 0, 0, acctstr
)
143 s
.location
= len(acctstr
)
145 def setwindow(s
, window
, key
):
149 for child
in s
.children
:
150 child
.update(window
, 0, s
.location
)
153 def getnewthreadframe(s
):
154 tf
= CursesThreadFrame(s
.c
, s
.ui
, s
.window
, 0, s
.location
)
156 s
.children
.append(tf
)
159 def startsleep(s
, sleepsecs
):
162 def sleeping(s
, sleepsecs
, remainingsecs
):
166 s
.drawleadstr(remainingsecs
)
170 time
.sleep(sleepsecs
)
178 return s
.sleeping_abort
183 class CursesThreadFrame
:
184 def __init__(s
, master
, ui
, window
, y
, x
):
185 """master should be a CursesUtil object."""
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
)}
207 def setcolor(self
, color
):
208 self
.color
= self
.colormap
[color
]
209 self
.colorname
= color
214 if self
.getcolor() == 'black':
215 self
.window
.addstr(self
.y
, self
.x
, ' ', self
.color
)
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
)
223 return self
.colorname
225 def getcolorpair(self
):
228 def update(self
, window
, y
, x
):
234 def setthread(self
, newthread
):
235 self
.setcolor('black')
237 # self.setcolor('gray')
239 # self.setcolor('black')
242 def __init__(s
, util
):
247 s
.statuslock
= Lock()
252 s
.thread
= threadutil
.ExitNotifyThread(target
= s
.bgreaderloop
,
253 name
= "InputHandler loop")
254 s
.thread
.setDaemon(1)
259 s
.statuslock
.acquire()
260 if s
.lockheld
or s
.bgchar
== None:
261 s
.statuslock
.release()
264 s
.statuslock
.release()
265 ch
= s
.c
.stdscr
.getch()
266 s
.statuslock
.acquire()
268 if s
.lockheld
or s
.bgchar
== None:
273 s
.statuslock
.release()
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.
279 callback is a function taking a single arg -- the char pressed.
281 If callback is None, clears the request."""
282 s
.statuslock
.acquire()
283 oldhandler
= s
.bgchar
284 newhandler
= callback
287 if oldhandler
and not newhandler
:
289 if newhandler
and not oldhandler
:
292 s
.statuslock
.release()
294 def input_acquire(s
):
295 """Call this method when you want exclusive input control.
296 Make sure to call input_release afterwards!
299 s
.inputlock
.acquire()
300 s
.statuslock
.acquire()
302 s
.statuslock
.release()
304 def input_release(s
):
305 """Call this method when you are done getting input."""
306 s
.statuslock
.acquire()
308 s
.statuslock
.release()
309 s
.inputlock
.release()
312 class Blinkenlights(BlinkenBase
, UIBase
):
318 BlinkenBase
.init_banner(s
)
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()
328 def resizehandler(s
, signum
, frame
):
331 def resizeterm(s
, dosleep
= 1):
332 if not s
.resizelock
.acquire(0):
335 signal
.signal(signal
.SIGWINCH
, signal
.SIG_IGN
)
345 s
.resizelock
.release()
346 signal
.signal(signal
.SIGWINCH
, s
.resizehandler
)
352 # Not a terminal? Can't use curses.
353 if not sys
.stdout
.isatty() and sys
.stdin
.isatty():
356 # No TERM specified? Can't use curses.
358 if not len(os
.environ
['TERM']):
362 # ncurses doesn't want to start? Can't use curses.
363 # This test is nasty because initscr() actually EXITS on error.
369 return not os
.WEXITSTATUS(os
.waitpid(pid
, 0)[1])
374 # If we didn't die by here, indicate success.
377 def keypress(s
, key
):
386 index
= acctkeys
.index(chr(key
))
388 # Key not a valid one: exit.
391 if index
>= len(s
.hotkeys
):
392 # Not in our list of valid hotkeys.
395 # Trying to end sleep somewhere.
397 s
.getaccountframe(s
.hotkeys
[index
]).syncnow()
399 def getpass(s
, accountname
, config
, errmsg
= None):
400 s
.inputhandler
.input_acquire()
402 # See comment on _msg for info on why both locks are obtained.
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()
416 s
.inputhandler
.input_release()
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)
427 s
.logwindow
.scrollok(1)
428 s
.logwindow
.move(s
.logheight
- 1, 0)
429 s
.setupwindow_drawlog()
430 accounts
= s
.af
.keys()
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
)
448 def setupwindow_drawbanner(s
):
450 color
= s
.c
.getpair(curses
.COLOR_WHITE
, curses
.COLOR_BLUE
) | \
453 color
= curses
.A_REVERSE
454 s
.bannerwindow
.bkgd(' ', color
) # Fill background with that color
455 s
.bannerwindow
.addstr("%s %s" % (version
.productname
,
457 s
.bannerwindow
.addstr(0, s
.bannerwindow
.getmaxyx()[1] - len(version
.copyright
) - 1,
460 s
.bannerwindow
.noutrefresh()
462 def setupwindow_drawlog(s
):
464 color
= s
.c
.getpair(curses
.COLOR_WHITE
, curses
.COLOR_BLACK
)
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()
472 def getaccountframe(s
, accountname
= None):
473 if accountname
== None:
474 accountname
= s
.getthreadaccount()
477 if accountname
in s
.af
:
478 return s
.af
[accountname
]
481 s
.af
[accountname
] = CursesAccountFrame(s
.c
, accountname
, s
)
490 return s
.af
[accountname
]
493 def _display(s
, msg
, color
= None):
495 for thisline
in msg
.split("\n"):
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)
503 # By locking both up-front here, in this order, we prevent deadlock.
508 if not s
.c
.isactive():
509 # For dumping out exceptions and stuff.
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()
522 def _addline(s
, msg
, color
):
525 s
.logwindow
.addstr("\n" + msg
, color
)
526 s
.text
.append((msg
, color
))
527 while len(s
.text
) > s
.logheight
:
532 def terminate(s
, exitstatus
= 0, errortitle
= None, errormsg
= None):
534 UIBase
.terminate(s
, exitstatus
= exitstatus
, errortitle
= errortitle
, errormsg
= errormsg
)
536 def threadException(s
, thread
):
538 UIBase
.threadException(s
, thread
)
540 def mainException(s
):
542 UIBase
.mainException(s
)
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
)
549 if __name__
== '__main__':
550 x
= Blinkenlights(None)
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
}
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] +
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
) | \
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
) | \