Complete.Org: Mailing Lists: Archives: offlineimap: September 2009:
[PATCH 1/2] furbished dynamic ui plugin selection
Home

[PATCH 1/2] furbished dynamic ui plugin selection

[Top] [All Lists]

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index] [Thread Index]
To: offlineimap@xxxxxxxxxxxx
Subject: [PATCH 1/2] furbished dynamic ui plugin selection
From: Christoph Höger <choeger@xxxxxxxxxxxxxxx>
Date: Fri, 4 Sep 2009 11:13:32 +0200

Here we go again, this time a lot better:
This patch allows ui plugins to be dropped only
inside the ui/plugins folder and work without modifying
any other file. Good for packagers.

* config:
Old config options like "Curses.Blinkenlights" still work
New ui plugins can use Modulename only if they provide a
getUIClass() method, I think thats a compromise

* default ui:
I have made Curses.Blinkenlights the default ui, but
perhaps TTY.TTYUI is a better choice for that.

* testing:
I tested against Blinkenlights and Gnome ui (which is not
yet in the mainline branch)

Signed-off-by: Christoph H=C3=B6ger <choeger@xxxxxxxxxxxxxxx>
---
 offlineimap/ui/Blinkenlights.py          |  147 ------
 offlineimap/ui/Curses.py                 |  593 -----------------------
 offlineimap/ui/Machine.py                |  179 -------
 offlineimap/ui/Noninteractive.py         |   51 --
 offlineimap/ui/TTY.py                    |   60 ---
 offlineimap/ui/__init__.py               |   17 -
 offlineimap/ui/detector.py               |   35 +-
 offlineimap/ui/plugins/Blinkenlights.py  |  143 ++++++
 offlineimap/ui/plugins/Curses.py         |  593 +++++++++++++++++++++++
 offlineimap/ui/plugins/Gnome.py          |  769 ++++++++++++++++++++++++=
++++++
 offlineimap/ui/plugins/Machine.py        |  177 +++++++
 offlineimap/ui/plugins/Noninteractive.py |   51 ++
 offlineimap/ui/plugins/TTY.py            |   60 +++
 offlineimap/ui/plugins/__init__.py       |   18 +
 14 files changed, 1831 insertions(+), 1062 deletions(-)
 delete mode 100644 offlineimap/ui/Blinkenlights.py
 delete mode 100644 offlineimap/ui/Curses.py
 delete mode 100644 offlineimap/ui/Machine.py
 delete mode 100644 offlineimap/ui/Noninteractive.py
 delete mode 100644 offlineimap/ui/TTY.py
 create mode 100644 offlineimap/ui/plugins/Blinkenlights.py
 create mode 100644 offlineimap/ui/plugins/Curses.py
 create mode 100644 offlineimap/ui/plugins/Gnome.py
 create mode 100644 offlineimap/ui/plugins/Machine.py
 create mode 100644 offlineimap/ui/plugins/Noninteractive.py
 create mode 100644 offlineimap/ui/plugins/TTY.py
 create mode 100644 offlineimap/ui/plugins/__init__.py

diff --git a/offlineimap/ui/Blinkenlights.py b/offlineimap/ui/Blinkenligh=
ts.py
deleted file mode 100644
index dcc4e01..0000000
--- a/offlineimap/ui/Blinkenlights.py
+++ /dev/null
@@ -1,147 +0,0 @@
-# Blinkenlights base classes
-# Copyright (C) 2003 John Goerzen
-# <jgoerzen@xxxxxxxxxxxx>
-#
-#    This program is free software; you can redistribute it and/or modif=
y
-#    it under the terms of the GNU General Public License as published b=
y
-#    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-13=
01 USA
-
-from threading import *
-from offlineimap.ui.UIBase import UIBase
-import thread
-from offlineimap.threadutil import MultiLock
-
-class BlinkenBase:
-    """This is a mix-in class that should be mixed in with either UIBase
-    or another appropriate base class.  The Tk interface, for instance,
-    will probably mix it in with VerboseUI."""
-
-    def acct(s, accountname):
-        s.gettf().setcolor('purple')
-        s.__class__.__bases__[-1].acct(s, accountname)
-
-    def connecting(s, hostname, port):
-        s.gettf().setcolor('gray')
-        s.__class__.__bases__[-1].connecting(s, hostname, port)
-
-    def syncfolders(s, srcrepos, destrepos):
-        s.gettf().setcolor('blue')
-        s.__class__.__bases__[-1].syncfolders(s, srcrepos, destrepos)
-
-    def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
-        s.gettf().setcolor('cyan')
-        s.__class__.__bases__[-1].syncingfolder(s, srcrepos, srcfolder, =
destrepos, destfolder)
-
-    def skippingfolder(s, folder):
-        s.gettf().setcolor('cyan')
-        s.__class__.__bases__[-1].skippingfolder(s, folder)
-
-    def loadmessagelist(s, repos, folder):
-        s.gettf().setcolor('green')
-        s._msg("Scanning folder [%s/%s]" % (s.getnicename(repos),
-                                            folder.getvisiblename()))
-
-    def syncingmessages(s, sr, sf, dr, df):
-        s.gettf().setcolor('blue')
-        s.__class__.__bases__[-1].syncingmessages(s, sr, sf, dr, df)
-
-    def copyingmessage(s, uid, src, destlist):
-        s.gettf().setcolor('orange')
-        s.__class__.__bases__[-1].copyingmessage(s, uid, src, destlist)
-
-    def deletingmessages(s, uidlist, destlist):
-        s.gettf().setcolor('red')
-        s.__class__.__bases__[-1].deletingmessages(s, uidlist, destlist)
-
-    def deletingmessage(s, uid, destlist):
-        s.gettf().setcolor('red')
-        s.__class__.__bases__[-1].deletingmessage(s, uid, destlist)
-
-    def addingflags(s, uidlist, flags, destlist):
-        s.gettf().setcolor('yellow')
-        s.__class__.__bases__[-1].addingflags(s, uidlist, flags, destlis=
t)
-
-    def deletingflags(s, uidlist, flags, destlist):
-        s.gettf().setcolor('pink')
-        s.__class__.__bases__[-1].deletingflags(s, uidlist, flags, destl=
ist)
-
-    def warn(s, msg, minor =3D 0):
-        if minor:
-            s.gettf().setcolor('pink')
-        else:
-            s.gettf().setcolor('red')
-        s.__class__.__bases__[-1].warn(s, msg, minor)
-
-    def init_banner(s):
-        s.availablethreadframes =3D {}
-        s.threadframes =3D {}
-        s.tflock =3D MultiLock()
-
-    def threadExited(s, thread):
-        threadid =3D thread.threadid
-        accountname =3D s.getthreadaccount(thread)
-        s.tflock.acquire()
-        try:
-            if threadid in s.threadframes[accountname]:
-                tf =3D s.threadframes[accountname][threadid]
-                del s.threadframes[accountname][threadid]
-                s.availablethreadframes[accountname].append(tf)
-                tf.setthread(None)
-        finally:
-            s.tflock.release()
-
-        UIBase.threadExited(s, thread)
-
-    def gettf(s):
-        threadid =3D thread.get_ident()
-        accountname =3D s.getthreadaccount()
-
-        s.tflock.acquire()
-
-        try:
-            if not accountname in s.threadframes:
-                s.threadframes[accountname] =3D {}
-               =20
-            if threadid in s.threadframes[accountname]:
-                return s.threadframes[accountname][threadid]
-
-            if not accountname in s.availablethreadframes:
-                s.availablethreadframes[accountname] =3D []
-
-            if len(s.availablethreadframes[accountname]):
-                tf =3D s.availablethreadframes[accountname].pop(0)
-                tf.setthread(currentThread())
-            else:
-                tf =3D s.getaccountframe().getnewthreadframe()
-            s.threadframes[accountname][threadid] =3D tf
-            return tf
-        finally:
-            s.tflock.release()
-
-    def callhook(s, msg):
-        s.gettf().setcolor('white')
-        s.__class__.__bases__[-1].callhook(s, msg)
-           =20
-    def sleep(s, sleepsecs, siglistener):
-        s.gettf().setcolor('red')
-        s.getaccountframe().startsleep(sleepsecs)
-        return UIBase.sleep(s, sleepsecs, siglistener)
-
-    def sleeping(s, sleepsecs, remainingsecs):
-        if remainingsecs and s.gettf().getcolor() =3D=3D 'black':
-            s.gettf().setcolor('red')
-        else:
-            s.gettf().setcolor('black')
-        return s.getaccountframe().sleeping(sleepsecs, remainingsecs)
-
-   =20
diff --git a/offlineimap/ui/Curses.py b/offlineimap/ui/Curses.py
deleted file mode 100644
index 8eb709f..0000000
--- a/offlineimap/ui/Curses.py
+++ /dev/null
@@ -1,593 +0,0 @@
-# Curses-based interfaces
-# Copyright (C) 2003 John Goerzen
-# <jgoerzen@xxxxxxxxxxxx>
-#
-#    This program is free software; you can redistribute it and/or modif=
y
-#    it under the terms of the GNU General Public License as published b=
y
-#    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-13=
01 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 =3D '1234567890abcdefghijklmnoprstuvwxyzABCDEFGHIJKLMNOPQRSTUVW=
XYZ-=3D;/.,'
-
-class CursesUtil:
-    def __init__(self):
-        self.pairlock =3D Lock()
-        self.iolock =3D MultiLock()
-        self.start()
-
-    def initpairs(self):
-        self.pairlock.acquire()
-        try:
-            self.pairs =3D {self._getpairindex(curses.COLOR_WHITE,
-                                             curses.COLOR_BLACK): 0}
-            self.nextpair =3D 1
-        finally:
-            self.pairlock.release()
-
-    def lock(self):
-        self.iolock.acquire()
-
-    def unlock(self):
-        self.iolock.release()
-       =20
-    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 =3D 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] =3D self.nextpair
-                curses.init_pair(self.nextpair, fg, bg)
-                self.nextpair +=3D 1
-                return curses.color_pair(self.nextpair - 1)
-        finally:
-            self.pairlock.release()
-   =20
-    def start(self):
-        self.stdscr =3D curses.initscr()
-        curses.noecho()
-        curses.cbreak()
-        self.stdscr.keypad(1)
-        try:
-            curses.start_color()
-            self.has_color =3D curses.has_colors()
-        except:
-            self.has_color =3D 0
-
-        self.oldcursor =3D None
-        try:
-            self.oldcursor =3D curses.curs_set(0)
-        except:
-            pass
-       =20
-        self.stdscr.clear()
-        self.stdscr.refresh()
-        (self.height, self.width) =3D 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 !=3D 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, ui):
-        s.c =3D master
-        s.children =3D []
-        s.accountname =3D accountname
-        s.ui =3D ui
-
-    def drawleadstr(s, secs =3D None):
-        if secs =3D=3D None:
-            acctstr =3D '%s: [active] %13.13s: ' % (s.key, s.accountname=
)
-        else:
-            acctstr =3D '%s: [%3d:%02d] %13.13s: ' % (s.key,
-                                                    secs / 60, secs % 60=
,
-                                                    s.accountname)
-        s.c.locked(s.window.addstr, 0, 0, acctstr)
-        s.location =3D len(acctstr)
-
-    def setwindow(s, window, key):
-        s.window =3D window
-        s.key =3D key
-        s.drawleadstr()
-        for child in s.children:
-            child.update(window, 0, s.location)
-            s.location +=3D 1
-
-    def getnewthreadframe(s):
-        tf =3D CursesThreadFrame(s.c, s.ui, s.window, 0, s.location)
-        s.location +=3D 1
-        s.children.append(tf)
-        return tf
-
-    def startsleep(s, sleepsecs):
-        s.sleeping_abort =3D 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 =3D 1
-
-class CursesThreadFrame:
-    def __init__(s, master, ui, window, y, x):
-        """master should be a CursesUtil object."""
-        s.c =3D master
-        s.ui =3D ui
-        s.window =3D window
-        s.x =3D x
-        s.y =3D y
-        s.colors =3D []
-        bg =3D curses.COLOR_BLACK
-        s.colormap =3D {'black': s.c.getpair(curses.COLOR_BLACK, bg),
-                         'gray': s.c.getpair(curses.COLOR_WHITE, bg),
-                         'white': curses.A_BOLD | s.c.getpair(curses.COL=
OR_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.CO=
LOR_YELLOW, bg),
-                         'pink': curses.A_BOLD | s.c.getpair(curses.COLO=
R_RED, bg)}
-        #s.setcolor('gray')
-        s.setcolor('black')
-
-    def setcolor(self, color):
-        self.color =3D self.colormap[color]
-        self.colorname =3D color
-        self.display()
-
-    def display(self):
-        def lockedstuff():
-            if self.getcolor() =3D=3D 'black':
-                self.window.addstr(self.y, self.x, ' ', self.color)
-            else:
-                self.window.addstr(self.y, self.x, self.ui.config.getdef=
ault("ui.Curses.Blinkenlights", "statuschar", '.'), 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 =3D window
-        self.y =3D y
-        self.x =3D 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 =3D util
-        s.bgchar =3D None
-        s.inputlock =3D Lock()
-        s.lockheld =3D 0
-        s.statuslock =3D Lock()
-        s.startup =3D Event()
-        s.startthread()
-
-    def startthread(s):
-        s.thread =3D threadutil.ExitNotifyThread(target =3D s.bgreaderlo=
op,
-                                               name =3D "InputHandler lo=
op")
-        s.thread.setDaemon(1)
-        s.thread.start()
-
-    def bgreaderloop(s):
-        while 1:
-            s.statuslock.acquire()
-            if s.lockheld or s.bgchar =3D=3D None:
-                s.statuslock.release()
-                s.startup.wait()
-            else:
-                s.statuslock.release()
-                ch =3D s.c.stdscr.getch()
-                s.statuslock.acquire()
-                try:
-                    if s.lockheld or s.bgchar =3D=3D 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 =3D s.bgchar
-        newhandler =3D callback
-        s.bgchar =3D callback
-
-        if oldhandler and not newhandler:
-            pass
-        if newhandler and not oldhandler:
-            s.startup.set()
-           =20
-        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 =3D 1
-        s.statuslock.release()
-
-    def input_release(s):
-        """Call this method when you are done getting input."""
-        s.statuslock.acquire()
-        s.lockheld =3D 0
-        s.statuslock.release()
-        s.inputlock.release()
-        s.startup.set()
-       =20
-class Blinkenlights(BlinkenBase, UIBase):
-    def init_banner(s):
-        s.af =3D {}
-        s.aflock =3D Lock()
-        s.c =3D CursesUtil()
-        s.text =3D []
-        BlinkenBase.init_banner(s)
-        s.setupwindows()
-        s.inputhandler =3D 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 =3D Lock()
-        s.resizecount =3D 0
-
-    def resizehandler(s, signum, frame):
-        s.resizeterm()
-
-    def resizeterm(s, dosleep =3D 1):
-        if not s.resizelock.acquire(0):
-            s.resizecount +=3D 1
-            return
-        signal.signal(signal.SIGWINCH, signal.SIG_IGN)
-        s.aflock.acquire()
-        s.c.lock()
-        s.resizecount +=3D 1
-        while s.resizecount:
-            s.c.reset()
-            s.setupwindows()
-            s.resizecount -=3D 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 =3D 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 < 1 or key > 255:
-            return
-       =20
-        if chr(key) =3D=3D 'q':
-            # Request to quit.
-            s.terminate()
-       =20
-        try:
-            index =3D acctkeys.index(chr(key))
-        except ValueError:
-            # Key not a valid one: exit.
-            return
-
-        if index >=3D 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 =3D None):
-        s.inputhandler.input_acquire()
-
-        # See comment on _msg for info on why both locks are obtained.
-       =20
-        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: " % a=
ccountname,
-                   s.gettf().getcolorpair())
-            s.logwindow.refresh()
-            password =3D 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 =3D curses.newwin(1, s.c.width, 0, 0)
-            s.setupwindow_drawbanner()
-            s.logheight =3D s.c.height - 1 - len(s.af.keys())
-            s.logwindow =3D 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 =3D s.af.keys()
-            accounts.sort()
-            accounts.reverse()
-
-            pos =3D s.c.height - 1
-            index =3D 0
-            s.hotkeys =3D []
-            for account in accounts:
-                accountwindow =3D curses.newwin(1, s.c.width, pos, 0)
-                s.af[account].setwindow(accountwindow, acctkeys[index])
-                s.hotkeys.append(account)
-                index +=3D 1
-                pos -=3D 1
-
-            curses.doupdate()
-        finally:
-            s.c.unlock()
-
-    def setupwindow_drawbanner(s):
-        if s.c.has_color:
-            color =3D s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLUE)=
 | \
-                    curses.A_BOLD
-        else:
-            color =3D curses.A_REVERSE
-        s.bannerwindow.bkgd(' ', color) # Fill background with that colo=
r
-        s.bannerwindow.addstr("%s %s" % (version.productname,
-                                         version.versionstr))
-        s.bannerwindow.addstr(0, s.bannerwindow.getmaxyx()[1] - len(vers=
ion.copyright) - 1,
-                              version.copyright)
-       =20
-        s.bannerwindow.noutrefresh()
-
-    def setupwindow_drawlog(s):
-        if s.c.has_color:
-            color =3D s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLACK=
)
-        else:
-            color =3D 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 =3D None):
-        if accountname =3D=3D None:
-            accountname =3D s.getthreadaccount()
-        s.aflock.acquire()
-        try:
-            if accountname in s.af:
-                return s.af[accountname]
-
-            # New one.
-            s.af[accountname] =3D CursesAccountFrame(s.c, accountname, s=
)
-            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 =3D 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 curse=
s)
-        #
-        # By locking both up-front here, in this order, we prevent deadl=
ock.
-       =20
-        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)
-            elif s.gettf().getcolor() =3D=3D 'black':
-                s.gettf().setcolor('gray')
-            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 =3D s.text[1:]
-        finally:
-            s.c.unlock()
-
-    def terminate(s, exitstatus =3D 0, errortitle =3D None, errormsg =3D=
 None):
-        s.c.stop()
-        UIBase.terminate(s, exitstatus =3D exitstatus, errortitle =3D er=
rortitle, errormsg =3D 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, siglistener):
-        s.gettf().setcolor('red')
-        s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60)=
)
-        return BlinkenBase.sleep(s, sleepsecs, siglistener)
-           =20
-if __name__ =3D=3D '__main__':
-    x =3D Blinkenlights(None)
-    x.init_banner()
-    import time
-    time.sleep(5)
-    x.c.stop()
-    fgs =3D {'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}
-   =20
-    x =3D CursesUtil()
-    win1 =3D 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 =3D 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 =3D 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 =3D 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)
-       =20
-       =20
-    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
-
diff --git a/offlineimap/ui/Machine.py b/offlineimap/ui/Machine.py
deleted file mode 100644
index 0a07e3e..0000000
--- a/offlineimap/ui/Machine.py
+++ /dev/null
@@ -1,179 +0,0 @@
-# Copyright (C) 2007 John Goerzen
-# <jgoerzen@xxxxxxxxxxxx>
-#
-#    This program is free software; you can redistribute it and/or modif=
y
-#    it under the terms of the GNU General Public License as published b=
y
-#    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-13=
01 USA
-
-import offlineimap.version
-import urllib, sys, re, time, traceback, threading, thread
-from UIBase import UIBase
-from threading import *
-
-protocol =3D '6.0.0'
-
-class MachineUI(UIBase):
-    def __init__(s, config, verbose =3D 0):
-        UIBase.__init__(s, config, verbose)
-        s.safechars=3D" ;,./-_=3D+()[]"
-        s.iswaiting =3D 0
-        s.outputlock =3D Lock()
-        s._printData('__init__', protocol)
-
-    def isusable(s):
-        return True
-
-    def _printData(s, command, data, dolock =3D True):
-        s._printDataOut('msg', command, data, dolock)
-
-    def _printWarn(s, command, data, dolock =3D True):
-        s._printDataOut('warn', command, data, dolock)
-
-    def _printDataOut(s, datatype, command, data, dolock =3D True):
-        if dolock:
-            s.outputlock.acquire()
-        try:
-            print "%s:%s:%s:%s" % \
-                    (datatype,
-                     urllib.quote(command, s.safechars),=20
-                     urllib.quote(currentThread().getName(), s.safechars=
),
-                     urllib.quote(data, s.safechars))
-            sys.stdout.flush()
-        finally:
-            if dolock:
-                s.outputlock.release()
-
-    def _display(s, msg):
-        s._printData('_display', msg)
-
-    def warn(s, msg, minor):
-        s._printData('warn', '%s\n%d' % (msg, int(minor)))
-
-    def registerthread(s, account):
-        UIBase.registerthread(s, account)
-        s._printData('registerthread', account)
-
-    def unregisterthread(s, thread):
-        UIBase.unregisterthread(s, thread)
-        s._printData('unregisterthread', thread.getName())
-
-    def debugging(s, debugtype):
-        s._printData('debugging', debugtype)
-
-    def acct(s, accountname):
-        s._printData('acct', accountname)
-
-    def acctdone(s, accountname):
-        s._printData('acctdone', accountname)
-
-    def validityproblem(s, folder):
-        s._printData('validityproblem', "%s\n%s\n%s\n%s" % \
-                (folder.getname(), folder.getrepository().getname(),
-                 folder.getsaveduidvalidity(), folder.getuidvalidity()))
-
-    def connecting(s, hostname, port):
-        s._printData('connecting', "%s\n%s" % (hostname, str(port)))
-
-    def syncfolders(s, srcrepos, destrepos):
-        s._printData('syncfolders', "%s\n%s" % (s.getnicename(srcrepos),=
=20
-                                                s.getnicename(destrepos)=
))
-
-    def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
-        s._printData('syncingfolder', "%s\n%s\n%s\n%s\n" % \
-                (s.getnicename(srcrepos), srcfolder.getname(),
-                 s.getnicename(destrepos), destfolder.getname()))
-
-    def loadmessagelist(s, repos, folder):
-        s._printData('loadmessagelist', "%s\n%s" % (s.getnicename(repos)=
,
-                                                    folder.getvisiblenam=
e()))
-
-    def messagelistloaded(s, repos, folder, count):
-        s._printData('messagelistloaded', "%s\n%s\n%d" % \
-                (s.getnicename(repos), folder.getname(), count))
-
-    def syncingmessages(s, sr, sf, dr, df):
-        s._printData('syncingmessages', "%s\n%s\n%s\n%s\n" % \
-                (s.getnicename(sr), sf.getname(), s.getnicename(dr),
-                 df.getname()))
-
-    def copyingmessage(s, uid, src, destlist):
-        ds =3D s.folderlist(destlist)
-        s._printData('copyingmessage', "%d\n%s\n%s\n%s"  % \
-                (uid, s.getnicename(src), src.getname(), ds))
-       =20
-    def folderlist(s, list):
-        return ("\f".join(["%s\t%s" % (s.getnicename(x), x.getname()) fo=
r x in list]))
-
-    def deletingmessage(s, uid, destlist):
-        s.deletingmessages(s, [uid], destlist)
-
-    def uidlist(s, list):
-        return ("\f".join([str(u) for u in list]))
-
-    def deletingmessages(s, uidlist, destlist):
-        ds =3D s.folderlist(destlist)
-        s._printData('deletingmessages', "%s\n%s" % (s.uidlist(uidlist),=
 ds))
-
-    def addingflags(s, uidlist, flags, destlist):
-        ds =3D s.folderlist(destlist)
-        s._printData("addingflags", "%s\n%s\n%s" % (s.uidlist(uidlist),
-                                                    "\f".join(flags),
-                                                    ds))
-
-    def deletingflags(s, uidlist, flags, destlist):
-        ds =3D s.folderlist(destlist)
-        s._printData('deletingflags', "%s\n%s\n%s" % (s.uidlist(uidlist)=
,
-                                                      "\f".join(flags),
-                                                      ds))
-
-    def threadException(s, thread):
-        print s.getThreadExceptionString(thread)
-        s._printData('threadException', "%s\n%s" % \
-                     (thread.getName(), s.getThreadExceptionString(threa=
d)))
-        s.delThreadDebugLog(thread)
-        s.terminate(100)
-
-    def terminate(s, exitstatus =3D 0, errortitle =3D '', errormsg =3D '=
'):
-        s._printData('terminate', "%d\n%s\n%s" % (exitstatus, errortitle=
, errormsg))
-        sys.exit(exitstatus)
-
-    def mainException(s):
-        s._printData('mainException', s.getMainExceptionString())
-
-    def threadExited(s, thread):
-        s._printData('threadExited', thread.getName())
-        UIBase.threadExited(s, thread)
-
-    def sleeping(s, sleepsecs, remainingsecs):
-        s._printData('sleeping', "%d\n%d" % (sleepsecs, remainingsecs))
-        if sleepsecs > 0:
-            time.sleep(sleepsecs)
-        return 0
-
-
-    def getpass(s, accountname, config, errmsg =3D None):
-        s.outputlock.acquire()
-        try:
-            if errmsg:
-                s._printData('getpasserror', "%s\n%s" % (accountname, er=
rmsg),
-                             False)
-            s._printData('getpass', accountname, False)
-            return (sys.stdin.readline()[:-1])
-        finally:
-            s.outputlock.release()
-
-    def init_banner(s):
-        s._printData('initbanner', offlineimap.version.banner)
-
-    def callhook(s, msg):
-        s._printData('callhook', msg)
diff --git a/offlineimap/ui/Noninteractive.py b/offlineimap/ui/Noninterac=
tive.py
deleted file mode 100644
index 9cd5eca..0000000
--- a/offlineimap/ui/Noninteractive.py
+++ /dev/null
@@ -1,51 +0,0 @@
-# Noninteractive UI
-# Copyright (C) 2002 John Goerzen
-# <jgoerzen@xxxxxxxxxxxx>
-#
-#    This program is free software; you can redistribute it and/or modif=
y
-#    it under the terms of the GNU General Public License as published b=
y
-#    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-13=
01 USA
-
-import sys, time
-from UIBase import UIBase
-
-class Basic(UIBase):
-    def getpass(s, accountname, config, errmsg =3D None):
-        raise NotImplementedError, "Prompting for a password is not supp=
orted in noninteractive mode."
-
-    def _display(s, msg):
-        print msg
-        sys.stdout.flush()
-
-    def warn(s, msg, minor =3D 0):
-        warntxt =3D 'WARNING'
-        if minor:
-            warntxt =3D 'warning'
-        sys.stderr.write(warntxt + ": " + str(msg) + "\n")
-
-    def sleep(s, sleepsecs, siglistener):
-        if s.verbose >=3D 0:
-            s._msg("Sleeping for %d:%02d" % (sleepsecs / 60, sleepsecs %=
 60))
-        return UIBase.sleep(s, sleepsecs, siglistener)
-
-    def sleeping(s, sleepsecs, remainingsecs):
-        if sleepsecs > 0:
-            time.sleep(sleepsecs)
-        return 0
-
-    def locked(s):
-        s.warn("Another OfflineIMAP is running with the same metadatadir=
; exiting.")
-
-class Quiet(Basic):
-    def __init__(s, config, verbose =3D -1):
-        Basic.__init__(s, config, verbose)
diff --git a/offlineimap/ui/TTY.py b/offlineimap/ui/TTY.py
deleted file mode 100644
index 99c46d4..0000000
--- a/offlineimap/ui/TTY.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# TTY UI
-# Copyright (C) 2002 John Goerzen
-# <jgoerzen@xxxxxxxxxxxx>
-#
-#    This program is free software; you can redistribute it and/or modif=
y
-#    it under the terms of the GNU General Public License as published b=
y
-#    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-13=
01 USA
-
-from UIBase import UIBase
-from getpass import getpass
-import select, sys
-from threading import *
-
-class TTYUI(UIBase):
-    def __init__(s, config, verbose =3D 0):
-        UIBase.__init__(s, config, verbose)
-        s.iswaiting =3D 0
-        s.outputlock =3D Lock()
-
-    def isusable(s):
-        return sys.stdout.isatty() and sys.stdin.isatty()
-       =20
-    def _display(s, msg):
-        s.outputlock.acquire()
-        try:
-            if (currentThread().getName() =3D=3D 'MainThread'):
-                print msg
-            else:
-                print "%s:\n   %s" % (currentThread().getName(), msg)
-            sys.stdout.flush()
-        finally:
-            s.outputlock.release()
-
-    def getpass(s, accountname, config, errmsg =3D None):
-        if errmsg:
-            s._msg("%s: %s" % (accountname, errmsg))
-        s.outputlock.acquire()
-        try:
-            return getpass("%s: Enter password: " % accountname)
-        finally:
-            s.outputlock.release()
-
-    def mainException(s):
-        if isinstance(sys.exc_info()[1], KeyboardInterrupt) and \
-           s.iswaiting:
-            sys.stdout.write("Timer interrupted at user request; program=
 terminating.             \n")
-            s.terminate()
-        else:
-            UIBase.mainException(s)
-
diff --git a/offlineimap/ui/__init__.py b/offlineimap/ui/__init__.py
index 0206ab4..081c1fc 100644
--- a/offlineimap/ui/__init__.py
+++ b/offlineimap/ui/__init__.py
@@ -16,23 +16,6 @@
 #    along with this program; if not, write to the Free Software
 #    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-13=
01 USA
=20
-
-import UIBase, Blinkenlights
-try:
-    import TTY
-except ImportError:
-    pass
-
-try:
-    import curses
-except ImportError:
-    pass
-else:
-    import Curses
-
-import Noninteractive
-import Machine
-
 # Must be last
 import detector
=20
diff --git a/offlineimap/ui/detector.py b/offlineimap/ui/detector.py
index 4ec7503..d73c9d0 100644
--- a/offlineimap/ui/detector.py
+++ b/offlineimap/ui/detector.py
@@ -19,17 +19,8 @@
 import offlineimap.ui
 import sys
=20
-DEFAULT_UI_LIST =3D ('Curses.Blinkenlights', 'TTY.TTYUI',
-                   'Noninteractive.Basic', 'Noninteractive.Quiet',
-                   'Machine.MachineUI')
-
 def findUI(config, chosenUI=3DNone):
-    uistrlist =3D list(DEFAULT_UI_LIST)
-    namespace=3D{}
-    for ui in dir(offlineimap.ui):
-        if ui.startswith('_') or ui in ('detector', 'UIBase'):
-            continue
-        namespace[ui]=3Dgetattr(offlineimap.ui, ui)
+    uistrlist =3D ["Curses.Blinkenlights"]
=20
     if chosenUI is not None:
         uistrlist =3D [chosenUI]
@@ -37,7 +28,7 @@ def findUI(config, chosenUI=3DNone):
         uistrlist =3D config.get("general", "ui").replace(" ", "").split=
(",")
=20
     for uistr in uistrlist:
-        uimod =3D getUImod(uistr, config.getlocaleval(), namespace)
+        uimod =3D getUImod(uistr)
         if uimod:
             uiinstance =3D uimod(config)
             if uiinstance.isusable():
@@ -45,10 +36,24 @@ def findUI(config, chosenUI=3DNone):
     sys.stderr.write("ERROR: No UIs were found usable!\n")
     sys.exit(200)
    =20
-def getUImod(uistr, localeval, namespace):
+def getUImod(uistr):
+    # ensure backwards compatibility with configs from <=3D 6.1
+    # uiClassName and dot will be empty if no . is found
+    uimodName,dot,uiClassName=3Duistr.partition(".")
+   =20
+    # this asserts that all elements are in place
     try:
-        uimod =3D localeval.eval(uistr, namespace)
+        # get the module from the plugin path
+        uimod =3D __import__("offlineimap.ui.plugins." + uimodName,[],[]=
,[uimodName])
+        if not uiClassName =3D=3D "":
+            # if uiClassName is set, we use the old method (introspectio=
n
+            # rocks!)
+            uiClass=3Duimod.__dict__[uiClassName]
+        else:
+            # asking the plugin for the class name is less error prone
+            uiClass =3D uimod.getUIClass()
     except (AttributeError, NameError), e:
-        #raise
+        print e
+       #raise
         return None
-    return uimod
+    return uiClass
diff --git a/offlineimap/ui/plugins/Blinkenlights.py b/offlineimap/ui/plu=
gins/Blinkenlights.py
new file mode 100644
index 0000000..6982351
--- /dev/null
+++ b/offlineimap/ui/plugins/Blinkenlights.py
@@ -0,0 +1,143 @@
+# Blinkenlights base classes
+# Copyright (C) 2003 John Goerzen
+# <jgoerzen@xxxxxxxxxxxx>
+#
+#    This program is free software; you can redistribute it and/or modif=
y
+#    it under the terms of the GNU General Public License as published b=
y
+#    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-13=
01 USA
+
+from threading import *
+from offlineimap.ui.UIBase import UIBase
+import thread
+from offlineimap.threadutil import MultiLock
+
+class BlinkenBase:
+    """This is a mix-in class that should be mixed in with either UIBase
+    or another appropriate base class.  The Tk interface, for instance,
+    will probably mix it in with VerboseUI."""
+
+    def acct(s, accountname):
+        s.gettf().setcolor('purple')
+        s.__class__.__bases__[-1].acct(s, accountname)
+
+    def connecting(s, hostname, port):
+        s.gettf().setcolor('gray')
+        s.__class__.__bases__[-1].connecting(s, hostname, port)
+
+    def syncfolders(s, srcrepos, destrepos):
+        s.gettf().setcolor('blue')
+        s.__class__.__bases__[-1].syncfolders(s, srcrepos, destrepos)
+
+    def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
+        s.gettf().setcolor('cyan')
+        s.__class__.__bases__[-1].syncingfolder(s, srcrepos, srcfolder, =
destrepos, destfolder)
+
+    def skippingfolder(s, folder):
+        s.gettf().setcolor('cyan')
+        s.__class__.__bases__[-1].skippingfolder(s, folder)
+
+    def loadmessagelist(s, repos, folder):
+        s.gettf().setcolor('green')
+        s._msg("Scanning folder [%s/%s]" % (s.getnicename(repos),
+                                            folder.getvisiblename()))
+
+    def syncingmessages(s, sr, sf, dr, df):
+        s.gettf().setcolor('blue')
+        s.__class__.__bases__[-1].syncingmessages(s, sr, sf, dr, df)
+
+    def copyingmessage(s, uid, src, destlist):
+        s.gettf().setcolor('orange')
+        s.__class__.__bases__[-1].copyingmessage(s, uid, src, destlist)
+
+    def deletingmessages(s, uidlist, destlist):
+        s.gettf().setcolor('red')
+        s.__class__.__bases__[-1].deletingmessages(s, uidlist, destlist)
+
+    def deletingmessage(s, uid, destlist):
+        s.gettf().setcolor('red')
+        s.__class__.__bases__[-1].deletingmessage(s, uid, destlist)
+
+    def addingflags(s, uidlist, flags, destlist):
+        s.gettf().setcolor('yellow')
+        s.__class__.__bases__[-1].addingflags(s, uidlist, flags, destlis=
t)
+
+    def deletingflags(s, uidlist, flags, destlist):
+        s.gettf().setcolor('pink')
+        s.__class__.__bases__[-1].deletingflags(s, uidlist, flags, destl=
ist)
+
+    def warn(s, msg, minor =3D 0):
+        if minor:
+            s.gettf().setcolor('pink')
+        else:
+            s.gettf().setcolor('red')
+        s.__class__.__bases__[-1].warn(s, msg, minor)
+
+    def init_banner(s):
+        s.availablethreadframes =3D {}
+        s.threadframes =3D {}
+        s.tflock =3D MultiLock()
+
+    def threadExited(s, thread):
+        threadid =3D thread.threadid
+        accountname =3D s.getthreadaccount(thread)
+        s.tflock.acquire()
+        try:
+            if threadid in s.threadframes[accountname]:
+                tf =3D s.threadframes[accountname][threadid]
+                del s.threadframes[accountname][threadid]
+                s.availablethreadframes[accountname].append(tf)
+                tf.setthread(None)
+        finally:
+            s.tflock.release()
+
+        UIBase.threadExited(s, thread)
+
+    def gettf(s):
+        threadid =3D thread.get_ident()
+        accountname =3D s.getthreadaccount()
+
+        s.tflock.acquire()
+
+        try:
+            if not accountname in s.threadframes:
+                s.threadframes[accountname] =3D {}
+               =20
+            if threadid in s.threadframes[accountname]:
+                return s.threadframes[accountname][threadid]
+
+            if not accountname in s.availablethreadframes:
+                s.availablethreadframes[accountname] =3D []
+
+            if len(s.availablethreadframes[accountname]):
+                tf =3D s.availablethreadframes[accountname].pop(0)
+                tf.setthread(currentThread())
+            else:
+                tf =3D s.getaccountframe().getnewthreadframe()
+            s.threadframes[accountname][threadid] =3D tf
+            return tf
+        finally:
+            s.tflock.release()
+           =20
+    def sleep(s, sleepsecs):
+        s.gettf().setcolor('red')
+        s.getaccountframe().startsleep(sleepsecs)
+        UIBase.sleep(s, sleepsecs)
+
+    def sleeping(s, sleepsecs, remainingsecs):
+        if remainingsecs and s.gettf().getcolor() =3D=3D 'black':
+            s.gettf().setcolor('red')
+        else:
+            s.gettf().setcolor('black')
+        return s.getaccountframe().sleeping(sleepsecs, remainingsecs)
+
+   =20
diff --git a/offlineimap/ui/plugins/Curses.py b/offlineimap/ui/plugins/Cu=
rses.py
new file mode 100644
index 0000000..949edb8
--- /dev/null
+++ b/offlineimap/ui/plugins/Curses.py
@@ -0,0 +1,593 @@
+# Curses-based interfaces
+# Copyright (C) 2003 John Goerzen
+# <jgoerzen@xxxxxxxxxxxx>
+#
+#    This program is free software; you can redistribute it and/or modif=
y
+#    it under the terms of the GNU General Public License as published b=
y
+#    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-13=
01 USA
+
+from Blinkenlights import BlinkenBase
+from offlineimap.ui.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 =3D '1234567890abcdefghijklmnoprstuvwxyzABCDEFGHIJKLMNOPQRSTUVW=
XYZ-=3D;/.,'
+
+class CursesUtil:
+    def __init__(self):
+        self.pairlock =3D Lock()
+        self.iolock =3D MultiLock()
+        self.start()
+
+    def initpairs(self):
+        self.pairlock.acquire()
+        try:
+            self.pairs =3D {self._getpairindex(curses.COLOR_WHITE,
+                                             curses.COLOR_BLACK): 0}
+            self.nextpair =3D 1
+        finally:
+            self.pairlock.release()
+
+    def lock(self):
+        self.iolock.acquire()
+
+    def unlock(self):
+        self.iolock.release()
+       =20
+    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 =3D 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] =3D self.nextpair
+                curses.init_pair(self.nextpair, fg, bg)
+                self.nextpair +=3D 1
+                return curses.color_pair(self.nextpair - 1)
+        finally:
+            self.pairlock.release()
+   =20
+    def start(self):
+        self.stdscr =3D curses.initscr()
+        curses.noecho()
+        curses.cbreak()
+        self.stdscr.keypad(1)
+        try:
+            curses.start_color()
+            self.has_color =3D curses.has_colors()
+        except:
+            self.has_color =3D 0
+
+        self.oldcursor =3D None
+        try:
+            self.oldcursor =3D curses.curs_set(0)
+        except:
+            pass
+       =20
+        self.stdscr.clear()
+        self.stdscr.refresh()
+        (self.height, self.width) =3D 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 !=3D 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, ui):
+        s.c =3D master
+        s.children =3D []
+        s.accountname =3D accountname
+        s.ui =3D ui
+
+    def drawleadstr(s, secs =3D None):
+        if secs =3D=3D None:
+            acctstr =3D '%s: [active] %13.13s: ' % (s.key, s.accountname=
)
+        else:
+            acctstr =3D '%s: [%3d:%02d] %13.13s: ' % (s.key,
+                                                    secs / 60, secs % 60=
,
+                                                    s.accountname)
+        s.c.locked(s.window.addstr, 0, 0, acctstr)
+        s.location =3D len(acctstr)
+
+    def setwindow(s, window, key):
+        s.window =3D window
+        s.key =3D key
+        s.drawleadstr()
+        for child in s.children:
+            child.update(window, 0, s.location)
+            s.location +=3D 1
+
+    def getnewthreadframe(s):
+        tf =3D CursesThreadFrame(s.c, s.ui, s.window, 0, s.location)
+        s.location +=3D 1
+        s.children.append(tf)
+        return tf
+
+    def startsleep(s, sleepsecs):
+        s.sleeping_abort =3D 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 =3D 1
+
+class CursesThreadFrame:
+    def __init__(s, master, ui, window, y, x):
+        """master should be a CursesUtil object."""
+        s.c =3D master
+        s.ui =3D ui
+        s.window =3D window
+        s.x =3D x
+        s.y =3D y
+        s.colors =3D []
+        bg =3D curses.COLOR_BLACK
+        s.colormap =3D {'black': s.c.getpair(curses.COLOR_BLACK, bg),
+                         'gray': s.c.getpair(curses.COLOR_WHITE, bg),
+                         'white': curses.A_BOLD | s.c.getpair(curses.COL=
OR_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.CO=
LOR_YELLOW, bg),
+                         'pink': curses.A_BOLD | s.c.getpair(curses.COLO=
R_RED, bg)}
+        #s.setcolor('gray')
+        s.setcolor('black')
+
+    def setcolor(self, color):
+        self.color =3D self.colormap[color]
+        self.colorname =3D color
+        self.display()
+
+    def display(self):
+        def lockedstuff():
+            if self.getcolor() =3D=3D 'black':
+                self.window.addstr(self.y, self.x, ' ', self.color)
+            else:
+                self.window.addstr(self.y, self.x, self.ui.config.getdef=
ault("ui.Curses.Blinkenlights", "statuschar", '.'), 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 =3D window
+        self.y =3D y
+        self.x =3D 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 =3D util
+        s.bgchar =3D None
+        s.inputlock =3D Lock()
+        s.lockheld =3D 0
+        s.statuslock =3D Lock()
+        s.startup =3D Event()
+        s.startthread()
+
+    def startthread(s):
+        s.thread =3D threadutil.ExitNotifyThread(target =3D s.bgreaderlo=
op,
+                                               name =3D "InputHandler lo=
op")
+        s.thread.setDaemon(1)
+        s.thread.start()
+
+    def bgreaderloop(s):
+        while 1:
+            s.statuslock.acquire()
+            if s.lockheld or s.bgchar =3D=3D None:
+                s.statuslock.release()
+                s.startup.wait()
+            else:
+                s.statuslock.release()
+                ch =3D s.c.stdscr.getch()
+                s.statuslock.acquire()
+                try:
+                    if s.lockheld or s.bgchar =3D=3D 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 =3D s.bgchar
+        newhandler =3D callback
+        s.bgchar =3D callback
+
+        if oldhandler and not newhandler:
+            pass
+        if newhandler and not oldhandler:
+            s.startup.set()
+           =20
+        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 =3D 1
+        s.statuslock.release()
+
+    def input_release(s):
+        """Call this method when you are done getting input."""
+        s.statuslock.acquire()
+        s.lockheld =3D 0
+        s.statuslock.release()
+        s.inputlock.release()
+        s.startup.set()
+       =20
+class Blinkenlights(BlinkenBase, UIBase):
+    def init_banner(s):
+        s.af =3D {}
+        s.aflock =3D Lock()
+        s.c =3D CursesUtil()
+        s.text =3D []
+        BlinkenBase.init_banner(s)
+        s.setupwindows()
+        s.inputhandler =3D 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 =3D Lock()
+        s.resizecount =3D 0
+
+    def resizehandler(s, signum, frame):
+        s.resizeterm()
+
+    def resizeterm(s, dosleep =3D 1):
+        if not s.resizelock.acquire(0):
+            s.resizecount +=3D 1
+            return
+        signal.signal(signal.SIGWINCH, signal.SIG_IGN)
+        s.aflock.acquire()
+        s.c.lock()
+        s.resizecount +=3D 1
+        while s.resizecount:
+            s.c.reset()
+            s.setupwindows()
+            s.resizecount -=3D 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 =3D 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 < 1 or key > 255:
+            return
+       =20
+        if chr(key) =3D=3D 'q':
+            # Request to quit.
+            s.terminate()
+       =20
+        try:
+            index =3D acctkeys.index(chr(key))
+        except ValueError:
+            # Key not a valid one: exit.
+            return
+
+        if index >=3D 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 =3D None):
+        s.inputhandler.input_acquire()
+
+        # See comment on _msg for info on why both locks are obtained.
+       =20
+        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: " % a=
ccountname,
+                   s.gettf().getcolorpair())
+            s.logwindow.refresh()
+            password =3D 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 =3D curses.newwin(1, s.c.width, 0, 0)
+            s.setupwindow_drawbanner()
+            s.logheight =3D s.c.height - 1 - len(s.af.keys())
+            s.logwindow =3D 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 =3D s.af.keys()
+            accounts.sort()
+            accounts.reverse()
+
+            pos =3D s.c.height - 1
+            index =3D 0
+            s.hotkeys =3D []
+            for account in accounts:
+                accountwindow =3D curses.newwin(1, s.c.width, pos, 0)
+                s.af[account].setwindow(accountwindow, acctkeys[index])
+                s.hotkeys.append(account)
+                index +=3D 1
+                pos -=3D 1
+
+            curses.doupdate()
+        finally:
+            s.c.unlock()
+
+    def setupwindow_drawbanner(s):
+        if s.c.has_color:
+            color =3D s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLUE)=
 | \
+                    curses.A_BOLD
+        else:
+            color =3D curses.A_REVERSE
+        s.bannerwindow.bkgd(' ', color) # Fill background with that colo=
r
+        s.bannerwindow.addstr("%s %s" % (version.productname,
+                                         version.versionstr))
+        s.bannerwindow.addstr(0, s.bannerwindow.getmaxyx()[1] - len(vers=
ion.copyright) - 1,
+                              version.copyright)
+       =20
+        s.bannerwindow.noutrefresh()
+
+    def setupwindow_drawlog(s):
+        if s.c.has_color:
+            color =3D s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLACK=
)
+        else:
+            color =3D 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 =3D None):
+        if accountname =3D=3D None:
+            accountname =3D s.getthreadaccount()
+        s.aflock.acquire()
+        try:
+            if accountname in s.af:
+                return s.af[accountname]
+
+            # New one.
+            s.af[accountname] =3D CursesAccountFrame(s.c, accountname, s=
)
+            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 =3D 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 curse=
s)
+        #
+        # By locking both up-front here, in this order, we prevent deadl=
ock.
+       =20
+        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)
+            elif s.gettf().getcolor() =3D=3D 'black':
+                s.gettf().setcolor('gray')
+            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 =3D s.text[1:]
+        finally:
+            s.c.unlock()
+
+    def terminate(s, exitstatus =3D 0, errortitle =3D None, errormsg =3D=
 None):
+        s.c.stop()
+        UIBase.terminate(s, exitstatus =3D exitstatus, errortitle =3D er=
rortitle, errormsg =3D 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)
+           =20
+if __name__ =3D=3D '__main__':
+    x =3D Blinkenlights(None)
+    x.init_banner()
+    import time
+    time.sleep(5)
+    x.c.stop()
+    fgs =3D {'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}
+   =20
+    x =3D CursesUtil()
+    win1 =3D 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 =3D 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 =3D 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 =3D 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)
+       =20
+       =20
+    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
+
diff --git a/offlineimap/ui/plugins/Gnome.py b/offlineimap/ui/plugins/Gno=
me.py
new file mode 100644
index 0000000..369d68c
--- /dev/null
+++ b/offlineimap/ui/plugins/Gnome.py
@@ -0,0 +1,769 @@
+# -*- mode: python; coding: utf-8 -*-
+# Gnome UI
+# Copyright (C) 2008 David H=E4rdeman <david@xxxxxxxxxxx>
+#
+#    This program is free software; you can redistribute it and/or modif=
y
+#    it under the terms of the GNU General Public License as published b=
y
+#    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-13=
01 USA
+
+import sys, time, thread, threading
+if __name__ =3D=3D '__main__':
+       from offlineimap import imapserver, repository, folder, mbnames, thread=
util, version, syncmaster, accounts
+       from offlineimap.threadutil import MultiLock
+from offlineimap import version, threadutil
+from offlineimap.ui.UIBase import UIBase
+
+# Temporarily make gtk emit exceptions insted of warnings
+import warnings
+warnings.filterwarnings('error', module=3D'gtk')
+try:
+       import pygtk
+       pygtk.require('2.0')
+       import gobject
+       import gtk
+       import gnome
+       import gnomekeyring as gkey
+       if gtk.pygtk_version[0] !=3D 2 or gtk.pygtk_version[1] < 10:
+               raise Exception, 'Invalid pygtk version'
+       gtk.gdk.threads_init()
+       usable =3D True
+except:
+       usable =3D False
+=09
+warnings.resetwarnings()
+
+# FIXME: Custom icons
+GNOME_UI_ICON =3D 'stock_mail-send-receive'
+GNOME_UI_ACTIVE_ICONS =3D [ 'stock_mail-forward', 'stock_mail-reply' ]
+
+class GnomeUIAboutDialog(gtk.AboutDialog):
+       def __init__(self, ui):
+               self.ui =3D ui
+               self.closed =3D threading.Event()
+               self.closed.set()
+               gtk.AboutDialog.__init__(self)
+               gtk.about_dialog_set_url_hook(lambda dialog,url: \
+                                             gnome.url_show(url))
+               gtk.about_dialog_set_email_hook(lambda dialog,url: \
+                                               gnome.url_show("mailto:"; + url))
+
+               self.connect('delete-event', self.delete_cb)
+               self.connect('response', self.response_cb)
+               self.set_name(version.productname)
+               self.set_version(version.versionstr)
+               self.set_copyright(version.copyright)
+               self.set_license(version.license)
+               self.set_wrap_license(True)
+               self.set_website(version.homepage)
+               self.set_website_label(version.homepage)
+               author =3D "%s <%s>" % (version.author,
+                                     version.author_email)
+               self.set_authors([author])
+               self.set_logo_icon_name(GNOME_UI_ICON)
+               self.set_program_name(version.productname)
+
+       def response_cb(self, arg =3D None, argb =3D None):
+               self.ui.debug("GnomeUIAboutDialog.response_cb()")
+               self.close()
+               return False
+
+       def delete_cb(self, widget, event):
+               self.ui.debug("GnomeUIAboutDialog.delete_cb()")
+               # Prevents widget destruction
+               return True
+
+       def close(self):
+               self.ui.debug("GnomeUIAboutDialog.close()")
+               self.hide_all()
+               self.closed.set()
+
+       def open(self, parent=3DNone):
+               self.ui.debug("GnomeUIAboutDialog.open()")
+               self.show_all()
+               self.closed.clear()
+
+
+class GnomeUIStatusIcon(gtk.StatusIcon):
+       STATE_ACTIVE =3D 1
+       STATE_IDLE   =3D 2
+
+       def __init__(self, ui):
+               self.ui =3D ui
+               self.closed =3D threading.Event()
+               gtk.StatusIcon.__init__(self)
+               self.connect('activate', self.ui.log.open)
+               self.connect('popup-menu', self.popup_cb)
+               self.animate_counter =3D 0
+               self.state =3D None
+
+               self.menu =3D gtk.Menu()
+
+               self.mstart =3D gtk.ImageMenuItem("_Start Sync")
+               img =3D gtk.image_new_from_stock(gtk.STOCK_EXECUTE, 
gtk.ICON_SIZE_MENU=
)
+               self.mstart.set_image(img)
+               self.mstart.connect('activate', self.ui.stopsleep_cb)
+               self.mstart.set_sensitive(False)
+               self.menu.append(self.mstart)
+
+               mlog =3D gtk.ImageMenuItem("Show _Log")
+               img =3D gtk.image_new_from_stock(gtk.STOCK_INFO, 
gtk.ICON_SIZE_MENU)
+               mlog.set_image(img)
+               mlog.connect('activate', self.ui.log.open)
+               self.menu.append(mlog)
+
+               sep =3D gtk.SeparatorMenuItem()
+               self.menu.append(sep)
+
+               mabout =3D gtk.ImageMenuItem(gtk.STOCK_ABOUT)
+               mabout.connect('activate', self.ui.about.open)
+               self.menu.append(mabout)
+
+               mquit =3D gtk.ImageMenuItem(gtk.STOCK_QUIT)
+               mquit.connect('activate', self.quit_cb)
+               self.menu.append(mquit)
+
+               self.set_state(GnomeUIStatusIcon.STATE_ACTIVE)
+               self.set_visible(True)
+
+       def set_title(self, title):
+               self.ui.debug("GnomeUIStatusIcon.set_title(%s)" % title)
+               self.set_tooltip(version.productname + ": " + title)
+
+       def set_state(self, state):
+               self.ui.debug("GnomeUIStatusIcon.set_state(%i)" % state)
+               if self.state =3D=3D state:
+                       return
+
+               if state =3D=3D GnomeUIStatusIcon.STATE_ACTIVE:
+                       gobject.timeout_add(500, self.animate_cb)
+                       self.set_title("active")
+                       self.mstart.set_sensitive(False)
+                       self.state =3D state
+               elif state =3D=3D GnomeUIStatusIcon.STATE_IDLE:
+                       self.set_from_icon_name(GNOME_UI_ICON)
+                       self.set_title("idle")
+                       self.mstart.set_sensitive(True)
+                       self.state =3D state
+
+       def quit_cb(self, notused =3D None):
+               self.ui.debug("GnomeUIStatusIcon.quit_cb()")
+               self.close()
+               self.ui.uiexit =3D True
+               self.ui.quit_cb()
+
+       def animate_cb(self):
+               self.ui.debug("GnomeUIStatusIcon.animate_cb()")
+               if self.state !=3D GnomeUIStatusIcon.STATE_ACTIVE:
+                       return False
+
+               icon =3D GNOME_UI_ACTIVE_ICONS[self.animate_counter % 
len(GNOME_UI_ACT=
IVE_ICONS)]
+               self.set_from_icon_name(icon)
+               self.animate_counter +=3D 1
+               return True
+
+       def popup_cb(self, widget, button, time, data =3D None):
+               self.ui.debug("GnomeUIStatusIcon.popup_cb()")
+               if button !=3D 3:
+                       return
+               self.menu.show_all()
+               self.menu.popup(None, None, None, 3, time)
+               return False
+
+       def close(self):
+               self.ui.debug("GnomeUIStatusIcon.close()")
+               self.set_visible(False)
+               self.closed.set()
+
+       def open(self):
+               self.ui.debug("GnomeUIStatusIcon.open()")
+               self.set_visible(True)
+               self.closed.clear()
+
+
+
+class GnomeUIPasswordDialog(gtk.Dialog):
+       def __init__(self, ui):
+               # DIALOG =3D +------+---------+
+               #          | ICON | MESSAGE |
+               #          |      |         |
+               #          |      | INPUT   |
+               #          +------+---------+
+               #          |KEYRING QESTION |
+               #          +----------------+
+               #          |        BUTTONS |
+               #          +----------------+
+               self.ui =3D ui
+               self.closed =3D threading.Event()
+               self.closed.set()
+               self.pw =3D None
+               self.pwready =3D threading.Event()
+               gtk.Dialog.__init__(self,
+                                   version.productname + \
+                                   " password prompt",
+                                   None,
+                                   gtk.DIALOG_NO_SEPARATOR,
+                                   # BUTTONS
+                                   (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
+                                    gtk.STOCK_OK, gtk.RESPONSE_OK))
+               self.set_decorated(False)
+               self.set_resizable(False)
+               self.set_position(gtk.WIN_POS_CENTER_ALWAYS)
+               self.set_border_width(16)
+               self.connect('response', self.done_cb)
+
+               # Split between ICON and MESSAGE + INPUT
+               mainsplit =3D gtk.HBox(False, 8)
+               self.vbox.pack_start(mainsplit, False, False, 0)
+
+               # ICON
+               icon =3D 
gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION,
+                                               gtk.ICON_SIZE_DIALOG)
+               valign =3D gtk.VBox(False, 0) # to top-align the icon
+               valign.pack_start(icon, False, False, 0)
+               mainsplit.pack_start(valign, False, False, 0)
+
+               # Split between MESSAGE and INPUT
+               split =3D gtk.VBox(False, 0)
+               mainsplit.pack_start(split, True, True, 0)
+
+               # MESSAGE
+               self.msg =3D gtk.Label()
+               lalign =3D gtk.HBox(False, 0) # to left-align the label
+               lalign.pack_start(self.msg, False, False, 0)
+               split.pack_start(lalign, False, False, 0)
+
+               # INPUT
+               input =3D gtk.HBox(False, 16)
+               split.pack_start(input, True, True, 32)
+
+               pwlabel =3D gtk.Label("Password:")
+               input.pack_start(pwlabel, False, False, 0)
+
+               # KEYRING QUESTION
+               keyring =3D gtk.HBox(False, 16)
+               split.pack_start(keyring, True, True, 0)
+               self.save_button =3D gtk.CheckButton("Store password in 
keyring")
+               keyring.pack_start(self.save_button, False, False, 0)
+
+               self.pwentry =3D gtk.Entry()
+               self.pwentry.set_visibility(False)
+               self.pwentry.set_activates_default(True)
+               input.pack_start(self.pwentry, True, True, 0)
+
+       def done_cb(self, dialog, response):
+               self.ui.debug("GnomeUIPasswordDialog.done_cb()")
+               self.pw =3D None
+               self.save_pw =3D False
+               if response =3D=3D gtk.RESPONSE_OK:
+                       self.save_pw =3D self.save_button.get_active()
+                       self.pw =3D self.pwentry.get_text()
+               self.pwready.set()
+               self.close()
+               return False
+
+       def close(self):
+               self.ui.debug("GnomeUIPasswordDialog.close()")
+               self.hide_all()
+               self.closed.set()
+
+       def open(self, markup =3D ''):
+               self.ui.debug("GnomeUIPasswordDialog.open()")
+               self.pwentry.set_text('')
+               self.pwready.clear()
+               self.msg.set_markup(markup)
+               self.set_default_response(gtk.RESPONSE_OK)
+               self.show_all()
+               self.closed.clear()
+
+
+class GnomeUILogWindow(gtk.Dialog):
+       def __init__(self, ui):
+               self.ui =3D ui
+               self.closed =3D threading.Event()
+               self.closed.set()
+               self.max_lines =3D 64 * 1024
+               self.autoscroll =3D True
+               gtk.Dialog.__init__(self,
+                                   version.productname + " log",
+                                   None,
+                                   0,
+                                   #gtk.DIALOG_NO_SEPARATOR,
+                                   (gtk.STOCK_CLEAR, gtk.RESPONSE_CANCEL,
+                                    gtk.STOCK_OK, gtk.RESPONSE_OK))
+               self.set_default_size(640, 480)
+               self.connect('response', self.response_cb)
+               self.connect('delete-event',self.close_on_delete_cb)
+
+               box =3D gtk.VBox(False, 8)
+               self.vbox.pack_start(box, True, True, 0)
+
+               title =3D gtk.HBox(False, 8)
+               img =3D gtk.image_new_from_icon_name(GNOME_UI_ICON,
+                                                  gtk.ICON_SIZE_DIALOG)
+               label =3D gtk.Label()
+               label.set_markup("<big><b>" + \
+                                version.productname + " Log" + \
+                                "</b></big>\n" + \
+                                "Recent log messages:")
+               title.pack_start(img, False, False, 0)
+               title.pack_start(label, False, False, 0)
+               box.pack_start(title, False, False, 8)
+
+               self.sw =3D gtk.ScrolledWindow()
+               self.sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
+               self.tv =3D gtk.TextView()
+               self.tv.set_wrap_mode(gtk.WRAP_WORD)
+               self.tv.set_pixels_below_lines(6)
+               self.tv.set_pixels_inside_wrap(0)
+               self.tv.set_editable(False)
+               self.tv.set_cursor_visible(False)
+               self.sw.add(self.tv)
+               self.tb =3D self.tv.get_buffer()
+               self.tb.connect('insert-text', self.insert_cb)
+               box.pack_start(self.sw, True, True, 8)
+
+               abox =3D gtk.HBox(False, 8)
+               box.pack_start(abox, False, False, 16)
+
+               self.entry_autoscroll =3D gtk.CheckButton("_Autoscroll")
+               self.entry_autoscroll.set_active(self.autoscroll)
+               self.entry_autoscroll.connect('toggled', self.toggle_scroll_cb)
+               abox.pack_start(self.entry_autoscroll, True, True, 0)
+
+               label =3D gtk.Label("Max Lines:")
+               abox.pack_start(label, False, False, 0)
+
+               adj =3D gtk.Adjustment(self.max_lines, 1, 65536, 1, 1024, 1024)
+               self.entry_lines =3D gtk.SpinButton(adj, 1.0, 0)
+               self.entry_lines.set_numeric(True)
+               self.entry_lines.set_wrap(False)
+               self.entry_lines.set_snap_to_ticks(True)
+               self.entry_lines.connect('value-changed', self.line_change_cb)
+               abox.pack_start(self.entry_lines, False, False, 0)
+
+       def scroll(self):
+               self.ui.debug("GnomeUILogWindow.scroll()")
+               if self.autoscroll:
+                       end =3D self.tb.get_end_iter()
+                       self.tv.scroll_to_iter(end, 0.0, True, 1.0, 1.0)
+
+       def delete_lines(self):
+               self.ui.debug("GnomeUILogWindow.delete_lines()")
+               lines =3D self.tb.get_line_count()
+               diff =3D lines - self.max_lines - 1
+               if diff > 0:
+                       start =3D self.tb.get_start_iter()
+                       end =3D self.tb.get_iter_at_line(diff)
+                       self.tb.delete(start, end)
+
+       def line_change_cb(self, spinbutton):
+               self.ui.debug("GnomeUILogWindow.line_change_cb()")
+               self.max_lines =3D self.entry_lines.get_value_as_int()
+               self.delete_lines()
+
+       def toggle_scroll_cb(self, togglebutton):
+               self.ui.debug("GnomeUILogWindow.toggle_scroll_cb()")
+               self.autoscroll =3D togglebutton.get_active()
+               self.scroll()
+
+       def insert_cb(self, textbuffer, iter, text, length):
+               self.ui.debug("GnomeUILogWindow.insert_cb()")
+               self.scroll()
+
+       def add_msg(self, msg):
+               self.ui.debug("GnomeUILogWindow.add_msg()")
+               gtk.gdk.threads_enter()
+               end =3D self.tb.get_end_iter()
+               self.tb.insert(end, msg.strip() + "\n")
+               self.delete_lines()
+               # the insert-text signal will take care of scrolling...
+               gtk.gdk.threads_leave()
+               return False
+
+       def close_on_delete_cb(self, widget, event):
+               # response_cb will handle everything
+               return True
+
+       def response_cb(self, widget, response):
+               self.ui.debug("GnomeUILogWindow.response_cb()")
+               if response =3D=3D gtk.RESPONSE_CANCEL:
+                       self.tb.set_text('')
+                       return False
+               else:
+                       self.close()
+                       # Prevent widget destruction
+                       return True
+
+       def close(self):
+               self.ui.debug("GnomeUILogWindow.close()")
+               self.hide_all()
+               self.closed.set()
+
+       def open(self, notused =3D None):
+               self.ui.debug("GnomeUILogWindow.open()")
+               self.show_all()
+               self.closed.clear()
+
+
+class GnomeUIWarningWindow(gtk.Dialog):
+       def __init__(self, ui):
+               self.ui =3D ui
+               self.closed =3D threading.Event()
+               self.closed.set()
+               gtk.Dialog.__init__(self,
+                                   version.productname + " WARNING",
+                                   None,
+                                   0,
+                                   #gtk.DIALOG_NO_SEPARATOR,
+                                   (gtk.STOCK_OK, gtk.RESPONSE_OK))
+               self.set_default_size(640, 480)
+               self.connect('response', self.response_cb)
+
+               box =3D gtk.VBox(False, 8)
+               self.vbox.pack_start(box, True, True, 0)
+
+               title =3D gtk.HBox(False, 8)
+               img =3D gtk.image_new_from_stock(gtk.STOCK_DIALOG_WARNING,
+                                              gtk.ICON_SIZE_DIALOG)
+               label =3D gtk.Label()
+               label.set_markup("<big><b>" + \
+                                version.productname + " Warning" + \
+                                "</b></big>\n" + \
+                                "The following errors have occurred:")
+               title.pack_start(img, False, False, 0)
+               title.pack_start(label, False, False, 0)
+               box.pack_start(title, False, False, 8)
+
+               self.sw =3D gtk.ScrolledWindow()
+               self.sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
+               self.tv =3D gtk.TextView()
+               self.tv.set_wrap_mode(gtk.WRAP_WORD)
+               self.tv.set_pixels_below_lines(6)
+               self.tv.set_pixels_inside_wrap(0)
+               self.tv.set_editable(False)
+               self.tv.set_cursor_visible(False)
+               self.sw.add(self.tv)
+               self.tb =3D self.tv.get_buffer()
+               self.tb.connect('insert-text', self.insert_cb)
+               box.pack_start(self.sw, True, True, 8)
+
+       def insert_cb(self, textbuffer, iter, text, length):
+               self.ui.debug("GnomeUIWarningWindow.insert_cb()")
+               end =3D self.tb.get_end_iter()
+               self.tv.scroll_to_iter(end, 0.0, True, 1.0, 1.0)
+
+       def add_msg(self, msg):
+               self.ui.debug("GnomeUIWarningWindow.add_msg()")
+               end =3D self.tb.get_end_iter()
+               self.tb.insert(end, msg.strip() + "\n")
+               self.open()
+               return False
+
+       def response_cb(self, widget =3D None, event =3D None):
+               self.ui.debug("GnomeUIWarningWindow.response_cb()")
+               self.close()
+               # Prevent widget destruction
+               return True
+
+       def close(self):
+               self.ui.debug("GnomeUIWarningWindow.close()")
+               self.hide_all()
+               self.tb.set_text('')
+               self.closed.set()
+
+       def open(self):
+               self.ui.debug("GnomeUILogWindow.open()")
+               self.show_all()
+               self.closed.clear()
+
+
+class GnomeUIThread:
+       def __init__(self):
+               # General
+               self.dbg       =3D False
+               self.lock      =3D threading.Lock()
+               self.thread    =3D None
+               self.uiexit    =3D False # Was an exit action initiated by the 
UI?
+               self.stopsleep =3D threading.Event()
+               self.exitsync  =3D threading.Event()
+               gtk.window_set_default_icon_name(GNOME_UI_ICON)
+
+               # Widgets
+               self.about     =3D GnomeUIAboutDialog(self)
+               self.log       =3D GnomeUILogWindow(self)
+               self.warn      =3D GnomeUIWarningWindow(self)
+               self.pwdialog  =3D GnomeUIPasswordDialog(self)
+               self.icon      =3D GnomeUIStatusIcon(self)
+
+       def start(self):
+               self.debug("GnomeUIThread.start()")
+               self.lock.acquire()
+               if self.thread is None:
+                       self.exitsync.clear()
+                       self.thread =3D threadutil.ExitNotifyThread(target =3D 
self.uiloop,
+                                                                 name =3D 
"GnomeUIThread")
+                       self.thread.setDaemon(True)
+                       self.thread.start()
+               self.lock.release()
+
+       def stop(self):
+               self.debug("GnomeUIThread.stop()")
+               self.lock.acquire()
+               if self.thread is not None:
+                       gobject.idle_add(self.quit_cb, None)
+                       # self.thread.join won't work for some reason...
+                       self.exitsync.wait()
+                       self.thread =3D None
+               self.lock.release()
+=09
+       def get_keyring_pw(self, accountname):
+               server =3D accountname
+               protocol =3D "offlineimap"
+               try:
+                       gkey.get_default_keyring_sync()
+               except (gkey.NoKeyringDaemonError):
+                       return None
+               try:
+                       attrs =3D {"server": server, "protocol": protocol}
+                       items =3D 
gkey.find_items_sync(gkey.ITEM_NETWORK_PASSWORD, attrs)
+                       if len(items) > 0:
+                               return items[0].secret
+                       else:
+                               return None
+               except (gkey.DeniedError, gkey.NoMatchError):
+                       return None
+
+       def set_keyring_pw(self, accountname, pw):
+               name =3D "offlineimap"
+               server =3D accountname
+               protocol =3D "offlineimap"
+               attrs =3D {
+                       "user": name,
+                       "server": server,
+                       "protocol": protocol,
+               }
+               gkey.item_create_sync(gkey.get_default_keyring_sync(),
+                gkey.ITEM_NETWORK_PASSWORD, "offlineimap", attrs, pw, Tr=
ue)=09
+
+
+       def getpass(self, accountname, config, errmsg =3D None):
+               self.debug("GnomeUIThread.getpass()")
+               pw =3D None
+               self.lock.acquire()
+               if self.thread is not None:
+                       pw =3D self.get_keyring_pw(accountname)
+                       if pw is None:   =20
+                               gobject.idle_add(self.pw_cb, accountname, 
config, errmsg)
+                               self.pwdialog.pwready.wait()
+                               self.pwdialog.pwready.clear()
+                               pw =3D self.pwdialog.pw
+                               if self.pwdialog.save_pw:
+                                       self.set_keyring_pw(accountname, pw)
+                               self.pwdialog.pw =3D None
+       =09
+               self.lock.release()
+               return pw
+
+       def add_msg(self, msg):
+               self.debug("GnomeUIThread.add_msg(%s)" % msg)
+               self.lock.acquire()
+               if self.thread is not None:
+                       gobject.idle_add(self.log.add_msg, msg)
+               self.lock.release()
+
+       def add_warning(self, msg):
+               self.debug("GnomeUIThread.add_warning(%s)" % msg)
+               self.add_msg('WARNING: ' + msg)
+               self.lock.acquire()
+               if self.thread is not None:
+                       gobject.idle_add(self.warn.add_msg, msg)
+               self.lock.release()
+
+       # Internal methods follow
+       def enable_debug(self, enable =3D True):
+               self.dbg =3D enable
+
+       def debug(self, msg):
+               if self.dbg:
+                       sys.stderr.write("GnomeUI-DEBUG: " + msg + "\n")
+
+       def stopsleep_cb(self, data =3D None):
+               self.debug("GnomeUIThread.stopsleep_cb()")
+               self.stopsleep.set()
+               return False
+
+       basemarkup =3D "<big><b>Enter %s password</b></big>\n\n" + \
+               "A password is needed to access account \"%s\"."
+
+       basemarkuperr =3D basemarkup + "\n\n" + \
+               "<span foreground=3D\"red\">%s</span>"
+
+       def pw_cb(self, accountname, config =3D None, errmsg =3D None):
+               self.debug("GnomeUIThread.pw_cb()")
+
+               if errmsg is None:
+                       markup =3D self.basemarkup % \
+                               (version.productname,
+                                accountname)
+               else:
+                       markup =3D self.basemarkuperr % \
+                               (version.productname,
+                                accountname,
+                                errmsg)
+
+               self.pw =3D self.pwdialog.open(markup)
+               return False
+
+       def quit_cb(self, data =3D None):
+               self.debug("GnomeUIThread.quit_cb()")
+               self.about.close()
+               self.log.close()
+               self.warn.close()
+               self.pwdialog.close()
+               self.icon.close()
+               gtk.main_quit()
+               return False
+
+       def uiloop(self):
+               self.debug("GnomeUIThread.uiloop() - begin")
+               gtk.main()
+               self.debug("GnomeUIThread.uiloop() - end (%s)" % 
repr(self.uiexit))
+               if self.uiexit:
+                       thread.interrupt_main()
+               self.exitsync.set()
+               #thread.exit()
+
+
+class GnomeUI(UIBase):
+       def __init__(self, config, verbose =3D 0):
+               UIBase.__init__(self, config, verbose)
+
+       def isusable(self):
+               return usable
+
+       def init_banner(self):
+               self.ui =3D GnomeUIThread()
+               self.ui.start()
+               UIBase.init_banner(self)
+
+       def getpass(self, accountname, config, errmsg =3D None):
+               return self.ui.getpass(accountname, config, errmsg)
+
+       def _display(self, msg):
+               self.ui.add_msg(msg)
+
+       def warn(self, msg, minor =3D 0):
+               if minor:
+                       self.ui.add_msg('warning: ' + str(msg))
+               else:
+                       self.ui.add_warning(str(msg))
+
+       def sleep(self, secs):
+               self.ui.debug("GnomeUI.sleep(%i)" % secs)
+               msg =3D "Sleeping for %d minutes" % (secs / 60)
+               if secs % 60 > 0:
+                       msg +=3D " and %02d seconds" % (secs % 60)
+               self.ui.add_msg(msg)
+               self.ui.icon.set_state(GnomeUIStatusIcon.STATE_IDLE)
+               self.ui.stopsleep.clear()
+               UIBase.sleep(self, secs)
+
+       def sleeping(self, secs, remainingsecs):
+               # Return values
+               # 0 if timeout expired
+               # 1 if there is a request to cancel the timer
+               # 2 if there is a request to abort the program
+               self.ui.icon.set_title("sleeping for %dm%02ds"  % \
+                                      (remainingsecs / 60, remainingsecs % 60))
+               if secs > 0:
+                       self.ui.stopsleep.wait(secs)
+                       if self.ui.stopsleep.isSet():
+                               return 1
+               else:
+                       self.ui.icon.set_state(GnomeUIStatusIcon.STATE_ACTIVE)
+               return 0
+
+       def terminate(self, exitstatus =3D 0, errortitle =3D None, errormsg =3D=
 None):
+               self.ui.debug("Terminating")
+               if not self.ui.uiexit and errormsg is not None:
+                       if errortitle is not None:
+                               self.ui.add_warning('ERROR: 
%s\n\n%s\n'%(errortitle, errormsg))
+                       else:
+                               self.ui.add_warning('%s\n' % errormsg)
+
+               # Give the user a chance to read any warnings
+               self.ui.warn.closed.wait()
+               if not self.ui.uiexit:
+                       self.ui.stop()
+               UIBase.terminate(self, exitstatus, errortitle, errormsg)
+
+       def locked(self):
+               self.warn("Another OfflineIMAP is running with the same 
metadatadir; e=
xiting.", 1)
+
+def getUIClass() : return GnomeUI
+
+if __name__ =3D=3D '__main__':
+       import time
+       print "MAIN: Begin tests"
+
+
+       x =3D GnomeUI(None)
+       x.init_banner()
+       x.ui.enable_debug(True)
+
+       warnings =3D 5
+       messages =3D 5
+
+       x.warn("Printing %i Warnings" % warnings)
+       for i in range(warnings):
+               x.warn("Warning " + str(i + 1))
+               time.sleep(1)
+=09
+       x.warn("Sleeping 999 secs")
+       x.warn("Select \"Start Sync\" from status icon context menu to continue=
")
+       x.sleep(999)
+
+       x.warn("Showing log window")
+       x.ui.log.open()
+
+       x.warn("Printing %i Log Messages" % messages)
+       for i in range(messages):
+               x.ui.add_msg("Line " + str(i + 1) + " of log text")
+               time.sleep(1)
+       x.warn("Sleeping 10 secs")
+       x.sleep(10)
+
+       x.warn("Showing about - close to continue")
+       x.ui.about.open()
+       x.ui.about.closed.wait()
+
+       x.warn("Getting a password")
+       password =3D x.ui.getpass("TEST", None, None)
+       if password is not None:
+               x.warn("Password was " + password)
+       else:
+               x.warn("No password given")
+
+       x.warn("Sleeping 3 secs")
+       x.sleep(3)
+       x.warn("Trying a password again with error message")
+       password =3D x.ui.getpass("Test", None, "Error message")
+       if password is not None:
+               x.warn("Password was " + password)
+       else:
+               x.warn("No password given")
+
+       x.warn("Terminating, close warning window to exit")
+       x.terminate()
+
+
diff --git a/offlineimap/ui/plugins/Machine.py b/offlineimap/ui/plugins/M=
achine.py
new file mode 100644
index 0000000..c77a3c5
--- /dev/null
+++ b/offlineimap/ui/plugins/Machine.py
@@ -0,0 +1,177 @@
+# Copyright (C) 2007 John Goerzen
+# <jgoerzen@xxxxxxxxxxxx>
+#
+#    This program is free software; you can redistribute it and/or modif=
y
+#    it under the terms of the GNU General Public License as published b=
y
+#    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-13=
01 USA
+
+import offlineimap.version
+import urllib, sys, re, time, traceback, threading, thread
+from offlineimap.ui.UIBase import UIBase
+from threading import *
+
+protocol =3D '6.0.0'
+
+class MachineUI(UIBase):
+    def __init__(s, config, verbose =3D 0):
+        UIBase.__init__(s, config, verbose)
+        s.safechars=3D" ;,./-_=3D+()[]"
+        s.iswaiting =3D 0
+        s.outputlock =3D Lock()
+        s._printData('__init__', protocol)
+
+    def isusable(s):
+        return True
+
+    def _printData(s, command, data, dolock =3D True):
+        s._printDataOut('msg', command, data, dolock)
+
+    def _printWarn(s, command, data, dolock =3D True):
+        s._printDataOut('warn', command, data, dolock)
+
+    def _printDataOut(s, datatype, command, data, dolock =3D True):
+        if dolock:
+            s.outputlock.acquire()
+        try:
+            print "%s:%s:%s:%s" % \
+                    (datatype,
+                     urllib.quote(command, s.safechars),=20
+                     urllib.quote(currentThread().getName(), s.safechars=
),
+                     urllib.quote(data, s.safechars))
+            sys.stdout.flush()
+        finally:
+            if dolock:
+                s.outputlock.release()
+
+    def _display(s, msg):
+        s._printData('_display', msg)
+
+    def warn(s, msg, minor):
+        s._printData('warn', '%s\n%d' % (msg, int(minor)))
+
+    def registerthread(s, account):
+        UIBase.registerthread(s, account)
+        s._printData('registerthread', account)
+
+    def unregisterthread(s, thread):
+        UIBase.unregisterthread(s, thread)
+        s._printData('unregisterthread', thread.getName())
+
+    def debugging(s, debugtype):
+        s._printData('debugging', debugtype)
+
+    def acct(s, accountname):
+        s._printData('acct', accountname)
+
+    def acctdone(s, accountname):
+        s._printData('acctdone', accountname)
+
+    def validityproblem(s, folder):
+        s._printData('validityproblem', "%s\n%s\n%s\n%s" % \
+                (folder.getname(), folder.getrepository().getname(),
+                 folder.getsaveduidvalidity(), folder.getuidvalidity()))
+
+    def connecting(s, hostname, port):
+        s._printData('connecting', "%s\n%s" % (hostname, str(port)))
+
+    def syncfolders(s, srcrepos, destrepos):
+        s._printData('syncfolders', "%s\n%s" % (s.getnicename(srcrepos),=
=20
+                                                s.getnicename(destrepos)=
))
+
+    def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
+        s._printData('syncingfolder', "%s\n%s\n%s\n%s\n" % \
+                (s.getnicename(srcrepos), srcfolder.getname(),
+                 s.getnicename(destrepos), destfolder.getname()))
+
+    def loadmessagelist(s, repos, folder):
+        s._printData('loadmessagelist', "%s\n%s" % (s.getnicename(repos)=
,
+                                                    folder.getvisiblenam=
e()))
+
+    def messagelistloaded(s, repos, folder, count):
+        s._printData('messagelistloaded', "%s\n%s\n%d" % \
+                (s.getnicename(repos), folder.getname(), count))
+
+    def syncingmessages(s, sr, sf, dr, df):
+        s._printData('syncingmessages', "%s\n%s\n%s\n%s\n" % \
+                (s.getnicename(sr), sf.getname(), s.getnicename(dr),
+                 df.getname()))
+
+    def copyingmessage(s, uid, src, destlist):
+        ds =3D s.folderlist(destlist)
+        s._printData('copyingmessage', "%d\n%s\n%s\n%s"  % \
+                (uid, s.getnicename(src), src.getname(), ds))
+       =20
+    def folderlist(s, list):
+        return ("\f".join(["%s\t%s" % (s.getnicename(x), x.getname()) fo=
r x in list]))
+
+    def deletingmessage(s, uid, destlist):
+        s.deletingmessages(s, [uid], destlist)
+
+    def uidlist(s, list):
+        return ("\f".join([str(u) for u in list]))
+
+    def deletingmessages(s, uidlist, destlist):
+        ds =3D s.folderlist(destlist)
+        s._printData('deletingmessages', "%s\n%s" % (s.uidlist(uidlist),=
 ds))
+
+    def addingflags(s, uidlist, flags, destlist):
+        ds =3D s.folderlist(destlist)
+        s._printData("addingflags", "%s\n%s\n%s" % (s.uidlist(uidlist),
+                                                    "\f".join(flags),
+                                                    ds))
+
+    def deletingflags(s, uidlist, flags, destlist):
+        ds =3D s.folderlist(destlist)
+        s._printData('deletingflags', "%s\n%s\n%s" % (s.uidlist(uidlist)=
,
+                                                      "\f".join(flags),
+                                                      ds))
+
+    def threadException(s, thread):
+        print s.getThreadExceptionString(thread)
+        s._printData('threadException', "%s\n%s" % \
+                     (thread.getName(), s.getThreadExceptionString(threa=
d)))
+        s.delThreadDebugLog(thread)
+        s.terminate(100)
+
+    def terminate(s, exitstatus =3D 0, errortitle =3D '', errormsg =3D '=
'):
+        s._printData('terminate', "%d\n%s\n%s" % (exitstatus, errortitle=
, errormsg))
+        sys.exit(exitstatus)
+
+    def mainException(s):
+        s._printData('mainException', s.getMainExceptionString())
+
+    def threadExited(s, thread):
+        s._printData('threadExited', thread.getName())
+        UIBase.threadExited(s, thread)
+
+    def sleeping(s, sleepsecs, remainingsecs):
+        s._printData('sleeping', "%d\n%d" % (sleepsecs, remainingsecs))
+        if sleepsecs > 0:
+            time.sleep(sleepsecs)
+        return 0
+
+
+    def getpass(s, accountname, config, errmsg =3D None):
+        s.outputlock.acquire()
+        try:
+            if errmsg:
+                s._printData('getpasserror', "%s\n%s" % (accountname, er=
rmsg),
+                             False)
+            s._printData('getpass', accountname, False)
+            return (sys.stdin.readline()[:-1])
+        finally:
+            s.outputlock.release()
+
+    def init_banner(s):
+        s._printData('initbanner', offlineimap.version.banner)
+
diff --git a/offlineimap/ui/plugins/Noninteractive.py b/offlineimap/ui/pl=
ugins/Noninteractive.py
new file mode 100644
index 0000000..ca8ff5a
--- /dev/null
+++ b/offlineimap/ui/plugins/Noninteractive.py
@@ -0,0 +1,51 @@
+# Noninteractive UI
+# Copyright (C) 2002 John Goerzen
+# <jgoerzen@xxxxxxxxxxxx>
+#
+#    This program is free software; you can redistribute it and/or modif=
y
+#    it under the terms of the GNU General Public License as published b=
y
+#    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-13=
01 USA
+
+import sys, time
+from offlineimap.ui.UIBase import UIBase
+
+class Basic(UIBase):
+    def getpass(s, accountname, config, errmsg =3D None):
+        raise NotImplementedError, "Prompting for a password is not supp=
orted in noninteractive mode."
+
+    def _display(s, msg):
+        print msg
+        sys.stdout.flush()
+
+    def warn(s, msg, minor =3D 0):
+        warntxt =3D 'WARNING'
+        if minor:
+            warntxt =3D 'warning'
+        sys.stderr.write(warntxt + ": " + str(msg) + "\n")
+
+    def sleep(s, sleepsecs):
+        if s.verbose >=3D 0:
+            s._msg("Sleeping for %d:%02d" % (sleepsecs / 60, sleepsecs %=
 60))
+        UIBase.sleep(s, sleepsecs)
+
+    def sleeping(s, sleepsecs, remainingsecs):
+        if sleepsecs > 0:
+            time.sleep(sleepsecs)
+        return 0
+
+    def locked(s):
+        s.warn("Another OfflineIMAP is running with the same metadatadir=
; exiting.")
+
+class Quiet(Basic):
+    def __init__(s, config, verbose =3D -1):
+        Basic.__init__(s, config, verbose)
diff --git a/offlineimap/ui/plugins/TTY.py b/offlineimap/ui/plugins/TTY.p=
y
new file mode 100644
index 0000000..32d6279
--- /dev/null
+++ b/offlineimap/ui/plugins/TTY.py
@@ -0,0 +1,60 @@
+# TTY UI
+# Copyright (C) 2002 John Goerzen
+# <jgoerzen@xxxxxxxxxxxx>
+#
+#    This program is free software; you can redistribute it and/or modif=
y
+#    it under the terms of the GNU General Public License as published b=
y
+#    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-13=
01 USA
+
+from offlineimap.ui.UIBase import UIBase
+from getpass import getpass
+import select, sys
+from threading import *
+
+class TTYUI(UIBase):
+    def __init__(s, config, verbose =3D 0):
+        UIBase.__init__(s, config, verbose)
+        s.iswaiting =3D 0
+        s.outputlock =3D Lock()
+
+    def isusable(s):
+        return sys.stdout.isatty() and sys.stdin.isatty()
+       =20
+    def _display(s, msg):
+        s.outputlock.acquire()
+        try:
+            if (currentThread().getName() =3D=3D 'MainThread'):
+                print msg
+            else:
+                print "%s:\n   %s" % (currentThread().getName(), msg)
+            sys.stdout.flush()
+        finally:
+            s.outputlock.release()
+
+    def getpass(s, accountname, config, errmsg =3D None):
+        if errmsg:
+            s._msg("%s: %s" % (accountname, errmsg))
+        s.outputlock.acquire()
+        try:
+            return getpass("%s: Enter password: " % accountname)
+        finally:
+            s.outputlock.release()
+
+    def mainException(s):
+        if isinstance(sys.exc_info()[1], KeyboardInterrupt) and \
+           s.iswaiting:
+            sys.stdout.write("Timer interrupted at user request; program=
 terminating.             \n")
+            s.terminate()
+        else:
+            UIBase.mainException(s)
+
diff --git a/offlineimap/ui/plugins/__init__.py b/offlineimap/ui/plugins/=
__init__.py
new file mode 100644
index 0000000..1b42165
--- /dev/null
+++ b/offlineimap/ui/plugins/__init__.py
@@ -0,0 +1,18 @@
+# UI module directory
+# Copyright (C) 2002 John Goerzen
+# <jgoerzen@xxxxxxxxxxxx>
+#
+#    This program is free software; you can redistribute it and/or modif=
y
+#    it under the terms of the GNU General Public License as published b=
y
+#    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-13=
01 USA
+
--=20
1.6.2.5




[Prev in Thread] Current Thread [Next in Thread]
  • [PATCH 1/2] furbished dynamic ui plugin selection, Christoph Höger <=