]> git.ipfire.org Git - thirdparty/openembedded/openembedded-core-contrib.git/commitdiff
Resurrect alternative UIs
authorBob Foerster <robert@erafx.com>
Fri, 17 Dec 2010 15:20:39 +0000 (23:20 +0800)
committerChristopher Larson <kergoth@gmail.com>
Fri, 17 Dec 2010 15:33:32 +0000 (23:33 +0800)
The various alternative UIs have been updated to once again be functional
with the latest bitbake internals.  Each of the UIs still have much room for
functional improvement.

In particular, they have been updated to:
 - interact with the new process based server
 - handle the current set of events and notifications fired from the server
   and its associated subsystems

Signed-off-by: Bob Foerster <robert@erafx.com>
bin/bitbake
lib/bb/ui/crumbs/progress.py
lib/bb/ui/crumbs/runningbuild.py
lib/bb/ui/depexp.py
lib/bb/ui/goggle.py
lib/bb/ui/knotty.py
lib/bb/ui/ncurses.py

index fcf8ca56ca2dc87a835c4efd1bdb48caa70cfea1..fa7caf2180586c68d6497ed4d762971787f2e5ba 100755 (executable)
@@ -74,7 +74,7 @@ def get_ui(config):
         return getattr(module, interface).main
     except AttributeError:
         sys.exit("FATAL: Invalid user interface '%s' specified.\n"
-                 "Valid interfaces: ncurses, depexp, knotty [default]." % interface)
+                 "Valid interfaces: depexp, goggle, ncurses, knotty [default]." % interface)
 
 
 # Display bitbake/OE warnings via the BitBake.Warnings logger, ignoring others"""
index 8bd87108e6f3ce814818219bd7278eaa5efe2f86..36eca38294075f10dddfa7e24748e08bbe824808 100644 (file)
@@ -14,4 +14,4 @@ class ProgressBar(gtk.Dialog):
 
     def update(self, x, y):
         self.progress.set_fraction(float(x)/float(y))
-        self.progress.set_text("%d/%d (%2d %%)" % (x, y, x*100/y))
+        self.progress.set_text("%2d %%" % (x*100/y))
index 9730bfd472dffe2214e4b84c6e6f3b3a46caa79c..4703e6d844da08bf48389ea81b9d5944f86d76d6 100644 (file)
@@ -1,3 +1,4 @@
+
 #
 # BitBake Graphical GTK User Interface
 #
 
 import gtk
 import gobject
+import logging
+import time
+import urllib
+import urllib2
+
+class Colors(object):
+    OK = "#ffffff"
+    RUNNING = "#aaffaa"
+    WARNING ="#f88017"
+    ERROR = "#ffaaaa"
 
 class RunningBuildModel (gtk.TreeStore):
-    (COL_TYPE, COL_PACKAGE, COL_TASK, COL_MESSAGE, COL_ICON, COL_ACTIVE) = (0, 1, 2, 3, 4, 5)
+    (COL_LOG, COL_PACKAGE, COL_TASK, COL_MESSAGE, COL_ICON, COL_COLOR, COL_NUM_ACTIVE) = range(7)
+
     def __init__ (self):
         gtk.TreeStore.__init__ (self,
                                 gobject.TYPE_STRING,
@@ -30,7 +42,8 @@ class RunningBuildModel (gtk.TreeStore):
                                 gobject.TYPE_STRING,
                                 gobject.TYPE_STRING,
                                 gobject.TYPE_STRING,
-                                gobject.TYPE_BOOLEAN)
+                                gobject.TYPE_STRING,
+                                gobject.TYPE_INT)
 
 class RunningBuild (gobject.GObject):
     __gsignals__ = {
@@ -63,32 +76,42 @@ class RunningBuild (gobject.GObject):
         # for the message.
         if hasattr(event, 'pid'):
             pid = event.pid
-            if pid in self.pids_to_task:
-                (package, task) = self.pids_to_task[pid]
-                parent = self.tasks_to_iter[(package, task)]
+        if hasattr(event, 'process'):
+            pid = event.process
+
+        if pid and pid in self.pids_to_task:
+            (package, task) = self.pids_to_task[pid]
+            parent = self.tasks_to_iter[(package, task)]
 
-        if isinstance(event, bb.msg.MsgBase):
-            # Ignore the "Running task i of n .."
-            if (event._message.startswith ("Running task")):
+        if(isinstance(event, logging.LogRecord)):
+            if (event.msg.startswith ("Running task")):
                 return # don't add these to the list
 
-            # Set a pretty icon for the message based on it's type.
-            if isinstance(event, bb.msg.MsgWarn):
-                icon = "dialog-warning"
-            elif isinstance(event, bb.msg.MsgError):
+            if event.levelno >= logging.ERROR:
                 icon = "dialog-error"
+                color = Colors.ERROR
+            elif event.levelno >= logging.WARNING:
+                icon = "dialog-warning"
+                color = Colors.WARNING
             else:
                 icon = None
+                color = Colors.OK
+
+            # if we know which package we belong to, we'll append onto its list.
+            # otherwise, we'll jump to the top of the master list
+            if parent:
+                tree_add = self.model.append
+            else:
+                tree_add = self.model.prepend
+            tree_add(parent,
+                              (None,
+                               package,
+                               task,
+                               event.getMessage(),
+                               icon,
+                               color,
+                               0))
 
-            # Add the message to the tree either at the top level if parent is
-            # None otherwise as a descendent of a task.
-            self.model.append (parent,
-                               (event.__class__.__name__.split()[-1], # e.g. MsgWarn, MsgError
-                                package,
-                                task,
-                                event._message,
-                                icon,
-                                False))
         elif isinstance(event, bb.build.TaskStarted):
             (package, task) = (event._package, event._task)
 
@@ -101,76 +124,142 @@ class RunningBuild (gobject.GObject):
             if ((package, None) in self.tasks_to_iter):
                 parent = self.tasks_to_iter[(package, None)]
             else:
-                parent = self.model.append (None, (None,
+                parent = self.model.prepend(None, (None,
                                                    package,
                                                    None,
                                                    "Package: %s" % (package),
                                                    None,
-                                                   False))
+                                                   Colors.OK,
+                                                   0))
                 self.tasks_to_iter[(package, None)] = parent
 
             # Because this parent package now has an active child mark it as
             # such.
-            self.model.set(parent, self.model.COL_ICON, "gtk-execute")
+            # @todo if parent is already in error, don't mark it green
+            self.model.set(parent, self.model.COL_ICON, "gtk-execute",
+                           self.model.COL_COLOR, Colors.RUNNING)
 
             # Add an entry in the model for this task
             i = self.model.append (parent, (None,
                                             package,
                                             task,
                                             "Task: %s" % (task),
-                                            None,
-                                            False))
+                                            "gtk-execute",
+                                            Colors.RUNNING,
+                                            0))
+
+            # update the parent's active task count
+            num_active = self.model.get(parent, self.model.COL_NUM_ACTIVE)[0] + 1
+            self.model.set(parent, self.model.COL_NUM_ACTIVE, num_active)
 
             # Save out the iter so that we can find it when we have a message
             # that we need to attach to a task.
             self.tasks_to_iter[(package, task)] = i
 
-            # Mark this task as active.
-            self.model.set(i, self.model.COL_ICON, "gtk-execute")
-
         elif isinstance(event, bb.build.TaskBase):
+            current = self.tasks_to_iter[(package, task)]
+            parent = self.tasks_to_iter[(package, None)]
+
+            # remove this task from the parent's active count
+            num_active = self.model.get(parent, self.model.COL_NUM_ACTIVE)[0] - 1
+            self.model.set(parent, self.model.COL_NUM_ACTIVE, num_active)
 
             if isinstance(event, bb.build.TaskFailed):
-                # Mark the task as failed
-                i = self.tasks_to_iter[(package, task)]
-                self.model.set(i, self.model.COL_ICON, "dialog-error")
+                # Mark the task and parent as failed
+                icon = "dialog-error"
+                color = Colors.ERROR
 
-                # Mark the parent package as failed
-                i = self.tasks_to_iter[(package, None)]
-                self.model.set(i, self.model.COL_ICON, "dialog-error")
+                logfile = event.logfile
+                if logfile and os.path.exists(logfile):
+                    with open(logfile) as f:
+                        logdata = f.read()
+                        self.model.append(current, ('pastebin', None, None, logdata, 'gtk-error', Colors.OK, 0))
+
+                for i in (current, parent):
+                    self.model.set(i, self.model.COL_ICON, icon,
+                                   self.model.COL_COLOR, color)
             else:
+                icon = None
+                color = Colors.OK
+
                 # Mark the task as inactive
-                i = self.tasks_to_iter[(package, task)]
-                self.model.set(i, self.model.COL_ICON, None)
+                self.model.set(current, self.model.COL_ICON, icon,
+                               self.model.COL_COLOR, color)
 
-                # Mark the parent package as inactive
+                # Mark the parent package as inactive, but make sure to
+                # preserve error and active states
                 i = self.tasks_to_iter[(package, None)]
-                self.model.set(i, self.model.COL_ICON, None)
-
+                if self.model.get(parent, self.model.COL_ICON) != 'dialog-error':
+                    self.model.set(parent, self.model.COL_ICON, icon)
+                    if num_active == 0:
+                        self.model.set(parent, self.model.COL_COLOR, Colors.OK)
 
             # Clear the iters and the pids since when the task goes away the
             # pid will no longer be used for messages
             del self.tasks_to_iter[(package, task)]
             del self.pids_to_task[pid]
 
+        elif isinstance(event, bb.event.BuildStarted):
+
+            self.model.prepend(None, (None,
+                                      None,
+                                      None,
+                                      "Build Started (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'),
+                                      None,
+                                      Colors.OK,
+                                      0))
         elif isinstance(event, bb.event.BuildCompleted):
             failures = int (event._failures)
+            self.model.prepend(None, (None,
+                                      None,
+                                      None,
+                                      "Build Completed (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'),
+                                      None,
+                                      Colors.OK,
+                                      0))
 
             # Emit the appropriate signal depending on the number of failures
-            if (failures > 1):
+            if (failures >= 1):
                 self.emit ("build-failed")
             else:
                 self.emit ("build-succeeded")
 
+        elif isinstance(event, bb.event.CacheLoadStarted) and pbar:
+            pbar.set_title("Loading cache")
+            self.progress_total = event.total
+            pbar.update(0, self.progress_total)
+        elif isinstance(event, bb.event.CacheLoadProgress) and pbar:
+            pbar.update(event.current, self.progress_total)
+        elif isinstance(event, bb.event.CacheLoadCompleted) and pbar:
+            pbar.update(self.progress_total, self.progress_total)
+
+        elif isinstance(event, bb.event.ParseStarted) and pbar:
+            pbar.set_title("Processing recipes")
+            self.progress_total = event.total
+            pbar.update(0, self.progress_total)
         elif isinstance(event, bb.event.ParseProgress) and pbar:
-            x = event.sofar
-            y = event.total
-            if x == y:
-                pbar.hide()
-                return
-            pbar.update(x, y)
+            pbar.update(event.current, self.progress_total)
+        elif isinstance(event, bb.event.ParseCompleted) and pbar:
+            pbar.hide()
+
+        return
+
+
+def do_pastebin(text):
+    url = 'http://pastebin.com/api_public.php'
+    params = {'paste_code': text, 'paste_format': 'text'}
+
+    req = urllib2.Request(url, urllib.urlencode(params))
+    response = urllib2.urlopen(req)
+    paste_url = response.read()
+
+    return paste_url
+
 
 class RunningBuildTreeView (gtk.TreeView):
+    __gsignals__ = {
+        "button_press_event" : "override"
+        }
     def __init__ (self):
         gtk.TreeView.__init__ (self)
 
@@ -181,6 +270,42 @@ class RunningBuildTreeView (gtk.TreeView):
         self.append_column (col)
 
         # The message of the build.
-        renderer = gtk.CellRendererText ()
-        col = gtk.TreeViewColumn ("Message", renderer, text=3)
-        self.append_column (col)
+        self.message_renderer = gtk.CellRendererText ()
+        self.message_column = gtk.TreeViewColumn ("Message", self.message_renderer, text=3)
+        self.message_column.add_attribute(self.message_renderer, 'background', 5)
+        self.message_renderer.set_property('editable', 5)
+        self.append_column (self.message_column)
+
+    def do_button_press_event(self, event):
+        gtk.TreeView.do_button_press_event(self, event)
+
+        if event.button == 3:
+            selection = super(RunningBuildTreeView, self).get_selection()
+            (model, iter) = selection.get_selected()
+            if iter is not None:
+                can_paste = model.get(iter, model.COL_LOG)[0]
+                if can_paste == 'pastebin':
+                    # build a simple menu with a pastebin option
+                    menu = gtk.Menu()
+                    menuitem = gtk.MenuItem("Send log to pastebin")
+                    menu.append(menuitem)
+                    menuitem.connect("activate", self.pastebin_handler, (model, iter))
+                    menuitem.show()
+                    menu.show()
+                    menu.popup(None, None, None, event.button, event.time)
+
+    def pastebin_handler(self, widget, data):
+        """
+        Send the log data to pastebin, then add the new paste url to the
+        clipboard.
+        """
+        (model, iter) = data
+        paste_url = do_pastebin(model.get(iter, model.COL_MESSAGE)[0])
+
+        # @todo Provide visual feedback to the user that it is done and that
+        # it worked.
+        print paste_url
+
+        clipboard = gtk.clipboard_get()
+        clipboard.set_text(paste_url)
+        clipboard.store()
\ No newline at end of file
index 967f3079011e30263d4793c16c2ec652d57aef98..a76e5bd2b96aa9cca6d71cb4ebf0fe32f7a577d6 100644 (file)
@@ -19,6 +19,7 @@
 
 import gobject
 import gtk
+import Queue
 import threading
 import xmlrpclib
 import bb
@@ -32,6 +33,7 @@ from bb.ui.crumbs.progress import ProgressBar
 (TYPE_DEP, TYPE_RDEP) = (0, 1)
 (COL_DEP_TYPE, COL_DEP_PARENT, COL_DEP_PACKAGE) = (0, 1, 2)
 
+
 class PackageDepView(gtk.TreeView):
     def __init__(self, model, dep_type, label):
         gtk.TreeView.__init__(self)
@@ -52,6 +54,7 @@ class PackageDepView(gtk.TreeView):
         self.current = package
         self.filter_model.refilter()
 
+
 class PackageReverseDepView(gtk.TreeView):
     def __init__(self, model, label):
         gtk.TreeView.__init__(self)
@@ -69,6 +72,7 @@ class PackageReverseDepView(gtk.TreeView):
         self.current = package
         self.filter_model.refilter()
 
+
 class DepExplorer(gtk.Window):
     def __init__(self):
         gtk.Window.__init__(self)
@@ -88,9 +92,12 @@ class DepExplorer(gtk.Window):
         scrolled = gtk.ScrolledWindow()
         scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
         scrolled.set_shadow_type(gtk.SHADOW_IN)
+
         self.pkg_treeview = gtk.TreeView(self.pkg_model)
         self.pkg_treeview.get_selection().connect("changed", self.on_cursor_changed)
-        self.pkg_treeview.append_column(gtk.TreeViewColumn("Package", gtk.CellRendererText(), text=COL_PKG_NAME))
+        column = gtk.TreeViewColumn("Package", gtk.CellRendererText(), text=COL_PKG_NAME)
+        self.pkg_treeview.append_column(column)
+        column.set_sort_column_id(COL_PKG_NAME)
         pane.add1(scrolled)
         scrolled.add(self.pkg_treeview)
 
@@ -156,7 +163,6 @@ class DepExplorer(gtk.Window):
 
 
 def parse(depgraph, pkg_model, depends_model):
-
     for package in depgraph["pn"]:
         pkg_model.set(pkg_model.append(), COL_PKG_NAME, package)
 
@@ -174,6 +180,7 @@ def parse(depgraph, pkg_model, depends_model):
                               COL_DEP_PARENT, package,
                               COL_DEP_PACKAGE, rdepend)
 
+
 class gtkthread(threading.Thread):
     quit = threading.Event()
     def __init__(self, shutdown):
@@ -187,8 +194,8 @@ class gtkthread(threading.Thread):
         gtk.main()
         gtkthread.quit.set()
 
-def main(server, eventHandler):
 
+def main(server, eventHandler):
     try:
         cmdline = server.runCommand(["getCmdLineAction"])
         if not cmdline or cmdline[0] != "generateDotGraph":
@@ -212,27 +219,61 @@ def main(server, eventHandler):
     pbar = ProgressBar(dep)
     gtk.gdk.threads_leave()
 
+    progress_total = 0
     while True:
         try:
-            event = eventHandler.waitEvent(0.25)
+            try:
+                # We must get nonblocking here, else we'll never check the
+                # quit signal
+                event = eventHandler.get(False, 0.25)
+            except Queue.Empty:
+                pass
+            
             if gtkthread.quit.isSet():
+                server.runCommand(["stateStop"])
                 break
 
             if event is None:
                 continue
+
+            if isinstance(event, bb.event.CacheLoadStarted):
+                progress_total = event.total
+                gtk.gdk.threads_enter()
+                pbar.set_title("Loading Cache")
+                pbar.update(0, progress_total)
+                gtk.gdk.threads_leave()
+
+            if isinstance(event, bb.event.CacheLoadProgress):
+                x = event.current
+                gtk.gdk.threads_enter()
+                pbar.update(x, progress_total)
+                gtk.gdk.threads_leave()
+                continue
+
+            if isinstance(event, bb.event.CacheLoadCompleted):
+                gtk.gdk.threads_enter()
+                pbar.update(progress_total, progress_total)
+                gtk.gdk.threads_leave()
+                continue
+
+            if isinstance(event, bb.event.ParseStarted):
+                progress_total = event.total
+                gtk.gdk.threads_enter()
+                pbar.set_title("Processing recipes")
+                pbar.update(0, progress_total)
+                gtk.gdk.threads_leave()
+
             if isinstance(event, bb.event.ParseProgress):
-                x = event.sofar
-                y = event.total
-                if x == y:
-                    print(("\nParsing finished. %d cached, %d parsed, %d skipped, %d masked, %d errors."
-                        % ( event.cached, event.parsed, event.skipped, event.masked, event.errors)))
-                    pbar.hide()
-                    return
+                x = event.current
                 gtk.gdk.threads_enter()
-                pbar.update(x, y)
+                pbar.update(x, progress_total)
                 gtk.gdk.threads_leave()
                 continue
 
+            if isinstance(event, bb.event.ParseCompleted):
+                pbar.hide()
+                continue
+
             if isinstance(event, bb.event.DepTreeGenerated):
                 gtk.gdk.threads_enter()
                 parse(event._depgraph, dep.pkg_model, dep.depends_model)
@@ -240,16 +281,22 @@ def main(server, eventHandler):
 
             if isinstance(event, bb.command.CommandCompleted):
                 continue
+
             if isinstance(event, bb.command.CommandFailed):
                 print("Command execution failed: %s" % event.error)
                 return event.exitcode
+
             if isinstance(event, bb.command.CommandExit):
                 return event.exitcode
+
             if isinstance(event, bb.cooker.CookerExit):
                 break
 
             continue
-
+        except EnvironmentError as ioerror:
+            # ignore interrupted io
+            if ioerror.args[0] == 4:
+                pass
         except KeyboardInterrupt:
             if shutdown == 2:
                 print("\nThird Keyboard Interrupt, exit.\n")
index 40923ba88d7ec61e9d9ca29cdb49a2805463eae4..5d45f004ca12f57e1d2a054b37e1106336292022 100644 (file)
@@ -24,19 +24,34 @@ import xmlrpclib
 from bb.ui.crumbs.runningbuild import RunningBuildTreeView, RunningBuild
 from bb.ui.crumbs.progress import ProgressBar
 
+import Queue
+
+
 def event_handle_idle_func (eventHandler, build, pbar):
 
     # Consume as many messages as we can in the time available to us
-    event = eventHandler.getEvent()
-    while event:
-        build.handle_event (event, pbar)
-        event = eventHandler.getEvent()
+    try:
+        while 1:
+            event = eventHandler.get(False)
+            build.handle_event (event, pbar)
+    except Queue.Empty:
+        pass
 
     return True
 
 def scroll_tv_cb (model, path, iter, view):
     view.scroll_to_cell (path)
 
+
+# @todo hook these into the GUI so the user has feedback...
+def running_build_failed_cb (running_build):
+    pass
+
+
+def running_build_succeeded_cb (running_build):
+    pass
+
+
 class MainWindow (gtk.Window):
     def __init__ (self):
         gtk.Window.__init__ (self, gtk.WINDOW_TOPLEVEL)
@@ -49,6 +64,7 @@ class MainWindow (gtk.Window):
         self.set_default_size(640, 480)
         scrolled_window.add (self.cur_build_tv)
 
+
 def main (server, eventHandler):
     gobject.threads_init()
     gtk.gdk.threads_init()
@@ -61,9 +77,11 @@ def main (server, eventHandler):
     running_build = RunningBuild ()
     window.cur_build_tv.set_model (running_build.model)
     running_build.model.connect("row-inserted", scroll_tv_cb, window.cur_build_tv)
+    running_build.connect ("build-succeeded", running_build_succeeded_cb)
+    running_build.connect ("build-failed", running_build_failed_cb)
+
     try:
         cmdline = server.runCommand(["getCmdLineAction"])
-        print(cmdline)
         if not cmdline:
             return 1
         ret = server.runCommand(cmdline)
@@ -76,10 +94,18 @@ def main (server, eventHandler):
 
     # Use a timeout function for probing the event queue to find out if we
     # have a message waiting for us.
-    gobject.timeout_add (200,
+    gobject.timeout_add (100,
                          event_handle_idle_func,
                          eventHandler,
                          running_build,
                          pbar)
 
-    gtk.main()
+    try:
+        gtk.main()
+    except EnvironmentError as ioerror:
+        # ignore interrupted io
+        if ioerror.args[0] == 4:
+            pass
+    finally:
+        server.runCommand(["stateStop"])
+
index f3abe8c9909153650b507be49c487179e928dd0e..0b47136cb981a38356662c357286e4e5d76f2220 100644 (file)
@@ -226,6 +226,10 @@ def main(server, eventHandler):
 
             logger.error("Unknown event: %s", event)
 
+        except EnvironmentError as ioerror:
+            # ignore interrupted io
+            if ioerror.args[0] == 4:
+                pass
         except KeyboardInterrupt:
             if shutdown == 2:
                 print("\nThird Keyboard Interrupt, exit.\n")
index 1db4ec173b78824e945a3fa4ebf0378954333b5c..ab626c9dfc8b17c1a4aeec6ba51b887832b82912 100644 (file)
@@ -44,8 +44,9 @@
 
 """
 
-from __future__ import division
 
+from __future__ import division
+import logging
 import os, sys, curses, itertools, time
 import bb
 import xmlrpclib
@@ -243,32 +244,36 @@ class NCursesUI:
         exitflag = False
         while not exitflag:
             try:
-                event = eventHandler.waitEvent(0.25)
-                if not event:
-                    continue
+                event = eventHandler.get()
+
                 helper.eventHandler(event)
-                #mw.appendText("%s\n" % event[0])
                 if isinstance(event, bb.build.TaskBase):
                     mw.appendText("NOTE: %s\n" % event._message)
-                if isinstance(event, bb.msg.MsgDebug):
-                    mw.appendText('DEBUG: ' + event._message + '\n')
-                if isinstance(event, bb.msg.MsgNote):
-                    mw.appendText('NOTE: ' + event._message + '\n')
-                if isinstance(event, bb.msg.MsgWarn):
-                    mw.appendText('WARNING: ' + event._message + '\n')
-                if isinstance(event, bb.msg.MsgError):
-                    mw.appendText('ERROR: ' + event._message + '\n')
-                if isinstance(event, bb.msg.MsgFatal):
-                    mw.appendText('FATAL: ' + event._message + '\n')
+                if isinstance(event, logging.LogRecord):
+                    mw.appendText(logging.getLevelName(event.levelno) + ': ' + event.getMessage() + '\n')
+
+                if isinstance(event, bb.event.CacheLoadStarted):
+                    self.parse_total = event.total
+                if isinstance(event, bb.event.CacheLoadProgress):
+                    x = event.current
+                    y = self.parse_total
+                    mw.setStatus("Loading Cache:   %s [%2d %%]" % ( next(parsespin), x*100/y ) )
+                if isinstance(event, bb.event.CacheLoadCompleted):
+                    mw.setStatus("Idle")
+                    mw.appendText("Loaded %d entries from dependency cache.\n"
+                                % ( event.num_entries))
+
+                if isinstance(event, bb.event.ParseStarted):
+                    self.parse_total = event.total
                 if isinstance(event, bb.event.ParseProgress):
-                    x = event.sofar
-                    y = event.total
-                    if x == y:
-                        mw.setStatus("Idle")
-                        mw.appendText("Parsing finished. %d cached, %d parsed, %d skipped, %d masked."
+                    x = event.current
+                    y = self.parse_total
+                    mw.setStatus("Parsing Recipes: %s [%2d %%]" % ( next(parsespin), x*100/y ) )
+                if isinstance(event, bb.event.ParseCompleted):
+                    mw.setStatus("Idle")
+                    mw.appendText("Parsing finished. %d cached, %d parsed, %d skipped, %d masked.\n"
                                 % ( event.cached, event.parsed, event.skipped, event.masked ))
-                    else:
-                        mw.setStatus("Parsing: %s (%04d/%04d) [%2d %%]" % ( next(parsespin), x, y, x*100//y ) )
+
 #                if isinstance(event, bb.build.TaskFailed):
 #                    if event.logfile:
 #                        if data.getVar("BBINCLUDELOGS", d):
@@ -289,7 +294,9 @@ class NCursesUI:
 #                            bb.msg.error(bb.msg.domain.Build, "see log in %s" % logfile)
 
                 if isinstance(event, bb.command.CommandCompleted):
-                    exitflag = True
+                    # stop so the user can see the result of the build, but
+                    # also allow them to now exit with a single ^C
+                    shutdown = 2
                 if isinstance(event, bb.command.CommandFailed):
                     mw.appendText("Command execution failed: %s" % event.error)
                     time.sleep(2)
@@ -306,13 +313,18 @@ class NCursesUI:
                     if activetasks:
                         taw.appendText("Active Tasks:\n")
                         for task in activetasks.itervalues():
-                            taw.appendText(task["title"])
+                            taw.appendText(task["title"] + '\n')
                     if failedtasks:
                         taw.appendText("Failed Tasks:\n")
                         for task in failedtasks:
-                            taw.appendText(task["title"])
+                            taw.appendText(task["title"] + '\n')
 
                 curses.doupdate()
+            except EnvironmentError as ioerror:
+                # ignore interrupted io
+                if ioerror.args[0] == 4:
+                    pass
+
             except KeyboardInterrupt:
                 if shutdown == 2:
                     mw.appendText("Third Keyboard Interrupt, exit.\n")