# Curses-based interfaces # Copyright (C) 2003 John Goerzen # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA from Blinkenlights import BlinkenBase from UIBase import UIBase from threading import * import thread, time, sys, os, signal, time from offlineimap import version, threadutil from offlineimap.threadutil import MultiLock import curses, curses.panel, curses.textpad, curses.wrapper acctkeys = '1234567890abcdefghijklmnoprstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-=;/.,' class CursesUtil: def __init__(self): self.pairlock = Lock() self.iolock = MultiLock() self.start() def initpairs(self): self.pairlock.acquire() try: self.pairs = {self._getpairindex(curses.COLOR_WHITE, curses.COLOR_BLACK): 0} self.nextpair = 1 finally: self.pairlock.release() def lock(self): self.iolock.acquire() def unlock(self): self.iolock.release() def locked(self, target, *args, **kwargs): """Perform an operation with full locking.""" self.lock() try: apply(target, args, kwargs) finally: self.unlock() def refresh(self): def lockedstuff(): curses.panel.update_panels() curses.doupdate() self.locked(lockedstuff) def isactive(self): return hasattr(self, 'stdscr') def _getpairindex(self, fg, bg): return '%d/%d' % (fg,bg) def getpair(self, fg, bg): if not self.has_color: return 0 pindex = self._getpairindex(fg, bg) self.pairlock.acquire() try: if self.pairs.has_key(pindex): return curses.color_pair(self.pairs[pindex]) else: self.pairs[pindex] = self.nextpair curses.init_pair(self.nextpair, fg, bg) self.nextpair += 1 return curses.color_pair(self.nextpair - 1) finally: self.pairlock.release() def start(self): self.stdscr = curses.initscr() curses.noecho() curses.cbreak() self.stdscr.keypad(1) try: curses.start_color() self.has_color = curses.has_colors() except: self.has_color = 0 self.oldcursor = None try: self.oldcursor = curses.curs_set(0) except: pass self.stdscr.clear() self.stdscr.refresh() (self.height, self.width) = self.stdscr.getmaxyx() self.initpairs() def stop(self): if not hasattr(self, 'stdscr'): return #self.stdscr.addstr(self.height - 1, 0, "\n", # self.getpair(curses.COLOR_WHITE, # curses.COLOR_BLACK)) if self.oldcursor != None: curses.curs_set(self.oldcursor) self.stdscr.refresh() self.stdscr.keypad(0) curses.nocbreak() curses.echo() curses.endwin() del self.stdscr def reset(self): self.stop() self.start() class CursesAccountFrame: def __init__(s, master, accountname): s.c = master s.children = [] s.accountname = accountname def drawleadstr(s, secs = None): if secs == None: acctstr = '%s: [active] %13.13s: ' % (s.key, s.accountname) else: acctstr = '%s: [%3d:%02d] %13.13s: ' % (s.key, secs / 60, secs % 60, s.accountname) s.c.locked(s.window.addstr, 0, 0, acctstr) s.location = len(acctstr) def setwindow(s, window, key): s.window = window s.key = key s.drawleadstr() for child in s.children: child.update(window, 0, s.location) s.location += 1 def getnewthreadframe(s): tf = CursesThreadFrame(s.c, s.window, 0, s.location) s.location += 1 s.children.append(tf) return tf def startsleep(s, sleepsecs): s.sleeping_abort = 0 def sleeping(s, sleepsecs, remainingsecs): if remainingsecs: s.c.lock() try: s.drawleadstr(remainingsecs) s.window.refresh() finally: s.c.unlock() time.sleep(sleepsecs) else: s.c.lock() try: s.drawleadstr() s.window.refresh() finally: s.c.unlock() return s.sleeping_abort def syncnow(s): s.sleeping_abort = 1 class CursesThreadFrame: def __init__(s, master, window, y, x): """master should be a CursesUtil object.""" s.c = master s.window = window s.x = x s.y = y s.colors = [] bg = curses.COLOR_BLACK s.colormap = {'black': s.c.getpair(curses.COLOR_BLACK, bg), 'gray': s.c.getpair(curses.COLOR_WHITE, bg), 'white': curses.A_BOLD | s.c.getpair(curses.COLOR_WHITE, bg), 'blue': s.c.getpair(curses.COLOR_BLUE, bg), 'red': s.c.getpair(curses.COLOR_RED, bg), 'purple': s.c.getpair(curses.COLOR_MAGENTA, bg), 'cyan': s.c.getpair(curses.COLOR_CYAN, bg), 'green': s.c.getpair(curses.COLOR_GREEN, bg), 'orange': s.c.getpair(curses.COLOR_YELLOW, bg), 'yellow': curses.A_BOLD | s.c.getpair(curses.COLOR_YELLOW, bg), 'pink': curses.A_BOLD | s.c.getpair(curses.COLOR_RED, bg)} #s.setcolor('gray') s.setcolor('black') def setcolor(self, color): self.color = self.colormap[color] self.colorname = color self.display() def display(self): def lockedstuff(): if self.getcolor() == 'black': self.window.addstr(self.y, self.x, ' ', self.color) else: self.window.addstr(self.y, self.x, '.', self.color) self.c.stdscr.move(self.c.height - 1, self.c.width - 1) self.window.refresh() self.c.locked(lockedstuff) def getcolor(self): return self.colorname def getcolorpair(self): return self.color def update(self, window, y, x): self.window = window self.y = y self.x = x self.display() def setthread(self, newthread): self.setcolor('black') #if newthread: # self.setcolor('gray') #else: # self.setcolor('black') class InputHandler: def __init__(s, util): s.c = util s.bgchar = None s.inputlock = Lock() s.lockheld = 0 s.statuslock = Lock() s.startup = Event() s.startthread() def startthread(s): s.thread = threadutil.ExitNotifyThread(target = s.bgreaderloop, name = "InputHandler loop") s.thread.setDaemon(1) s.thread.start() def bgreaderloop(s): while 1: s.statuslock.acquire() if s.lockheld or s.bgchar == None: s.statuslock.release() s.startup.wait() else: s.statuslock.release() ch = s.c.stdscr.getch() s.statuslock.acquire() try: if s.lockheld or s.bgchar == None: curses.ungetch(ch) else: s.bgchar(ch) finally: s.statuslock.release() def set_bgchar(s, callback): """Sets a "background" character handler. If a key is pressed while not doing anything else, it will be passed to this handler. callback is a function taking a single arg -- the char pressed. If callback is None, clears the request.""" s.statuslock.acquire() oldhandler = s.bgchar newhandler = callback s.bgchar = callback if oldhandler and not newhandler: pass if newhandler and not oldhandler: s.startup.set() s.statuslock.release() def input_acquire(s): """Call this method when you want exclusive input control. Make sure to call input_release afterwards! """ s.inputlock.acquire() s.statuslock.acquire() s.lockheld = 1 s.statuslock.release() def input_release(s): """Call this method when you are done getting input.""" s.statuslock.acquire() s.lockheld = 0 s.statuslock.release() s.inputlock.release() s.startup.set() class Blinkenlights(BlinkenBase, UIBase): def init_banner(s): s.af = {} s.aflock = Lock() s.c = CursesUtil() s.text = [] BlinkenBase.init_banner(s) s.setupwindows() s.inputhandler = InputHandler(s.c) s.gettf().setcolor('red') s._msg(version.banner) s.inputhandler.set_bgchar(s.keypress) signal.signal(signal.SIGWINCH, s.resizehandler) s.resizelock = Lock() s.resizecount = 0 def resizehandler(s, signum, frame): s.resizeterm() def resizeterm(s, dosleep = 1): if not s.resizelock.acquire(0): s.resizecount += 1 return signal.signal(signal.SIGWINCH, signal.SIG_IGN) s.aflock.acquire() s.c.lock() s.resizecount += 1 while s.resizecount: s.c.reset() s.setupwindows() s.resizecount -= 1 s.c.unlock() s.aflock.release() s.resizelock.release() signal.signal(signal.SIGWINCH, s.resizehandler) if dosleep: time.sleep(1) s.resizeterm(0) def isusable(s): # Not a terminal? Can't use curses. if not sys.stdout.isatty() and sys.stdin.isatty(): return 0 # No TERM specified? Can't use curses. try: if not len(os.environ['TERM']): return 0 except: return 0 # ncurses doesn't want to start? Can't use curses. # This test is nasty because initscr() actually EXITS on error. # grr. pid = os.fork() if pid: # parent return not os.WEXITSTATUS(os.waitpid(pid, 0)[1]) else: # child curses.initscr() curses.endwin() # If we didn't die by here, indicate success. sys.exit(0) def keypress(s, key): if key > 255: return if chr(key) == 'q': # Request to quit. s.terminate() try: index = acctkeys.index(chr(key)) except ValueError: # Key not a valid one: exit. return if index >= len(s.hotkeys): # Not in our list of valid hotkeys. return # Trying to end sleep somewhere. s.getaccountframe(s.hotkeys[index]).syncnow() def getpass(s, accountname, config, errmsg = None): s.inputhandler.input_acquire() # See comment on _msg for info on why both locks are obtained. s.tflock.acquire() s.c.lock() try: s.gettf().setcolor('white') s._addline(" *** Input Required", s.gettf().getcolorpair()) s._addline(" *** Please enter password for account %s: " % accountname, s.gettf().getcolorpair()) s.logwindow.refresh() password = s.logwindow.getstr() finally: s.tflock.release() s.c.unlock() s.inputhandler.input_release() return password def setupwindows(s): s.c.lock() try: s.bannerwindow = curses.newwin(1, s.c.width, 0, 0) s.setupwindow_drawbanner() s.logheight = s.c.height - 1 - len(s.af.keys()) s.logwindow = curses.newwin(s.logheight, s.c.width, 1, 0) s.logwindow.idlok(1) s.logwindow.scrollok(1) s.logwindow.move(s.logheight - 1, 0) s.setupwindow_drawlog() accounts = s.af.keys() accounts.sort() accounts.reverse() pos = s.c.height - 1 index = 0 s.hotkeys = [] for account in accounts: accountwindow = curses.newwin(1, s.c.width, pos, 0) s.af[account].setwindow(accountwindow, acctkeys[index]) s.hotkeys.append(account) index += 1 pos -= 1 curses.doupdate() finally: s.c.unlock() def setupwindow_drawbanner(s): if s.c.has_color: color = s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLUE) | \ curses.A_BOLD else: color = curses.A_REVERSE s.bannerwindow.bkgd(' ', color) # Fill background with that color s.bannerwindow.addstr("%s %s" % (version.productname, version.versionstr)) s.bannerwindow.addstr(0, s.bannerwindow.getmaxyx()[1] - len(version.copyright) - 1, version.copyright) s.bannerwindow.noutrefresh() def setupwindow_drawlog(s): if s.c.has_color: color = s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLACK) else: color = curses.A_NORMAL s.logwindow.bkgd(' ', color) for line, color in s.text: s.logwindow.addstr("\n" + line, color) s.logwindow.noutrefresh() def getaccountframe(s, accountname = None): if accountname == None: accountname = s.getthreadaccount() s.aflock.acquire() try: if accountname in s.af: return s.af[accountname] # New one. s.af[accountname] = CursesAccountFrame(s.c, accountname) s.c.lock() try: s.c.reset() s.setupwindows() finally: s.c.unlock() finally: s.aflock.release() return s.af[accountname] def _display(s, msg, color = None): if "\n" in msg: for thisline in msg.split("\n"): s._msg(thisline) return # We must acquire both locks. Otherwise, deadlock can result. # This can happen if one thread calls _msg (locking curses, then # tf) and another tries to set the color (locking tf, then curses) # # By locking both up-front here, in this order, we prevent deadlock. s.tflock.acquire() s.c.lock() try: if not s.c.isactive(): # For dumping out exceptions and stuff. print msg return if color: s.gettf().setcolor(color) s._addline(msg, s.gettf().getcolorpair()) s.logwindow.refresh() finally: s.c.unlock() s.tflock.release() def _addline(s, msg, color): s.c.lock() try: s.logwindow.addstr("\n" + msg, color) s.text.append((msg, color)) while len(s.text) > s.logheight: s.text = s.text[1:] finally: s.c.unlock() def terminate(s, exitstatus = 0, errortitle = None, errormsg = None): s.c.stop() if errormsg <> None: if errortitle <> None: sys.stderr.write('%s: %s\n'%(errortitle, errormsg)) else: sys.stderr.write('%s\n' % errormsg) UIBase.terminate(s, exitstatus = exitstatus, errortitle = errortitle, errormsg = errormsg) def threadException(s, thread): s.c.stop() UIBase.threadException(s, thread) def mainException(s): s.c.stop() UIBase.mainException(s) def sleep(s, sleepsecs): s.gettf().setcolor('red') s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60)) BlinkenBase.sleep(s, sleepsecs) if __name__ == '__main__': x = Blinkenlights(None) x.init_banner() import time time.sleep(5) x.c.stop() fgs = {'black': curses.COLOR_BLACK, 'red': curses.COLOR_RED, 'green': curses.COLOR_GREEN, 'yellow': curses.COLOR_YELLOW, 'blue': curses.COLOR_BLUE, 'magenta': curses.COLOR_MAGENTA, 'cyan': curses.COLOR_CYAN, 'white': curses.COLOR_WHITE} x = CursesUtil() win1 = curses.newwin(x.height, x.width / 4 - 1, 0, 0) win1.addstr("Black/normal\n") for name, fg in fgs.items(): win1.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLACK)) win2 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1]) win2.addstr("Blue/normal\n") for name, fg in fgs.items(): win2.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLUE)) win3 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1] + win2.getmaxyx()[1]) win3.addstr("Black/bright\n") for name, fg in fgs.items(): win3.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLACK) | \ curses.A_BOLD) win4 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1] * 3) win4.addstr("Blue/bright\n") for name, fg in fgs.items(): win4.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLUE) | \ curses.A_BOLD) win1.refresh() win2.refresh() win3.refresh() win4.refresh() x.stdscr.refresh() import time time.sleep(5) x.stop() print x.has_color print x.height print x.width