require 'trollop'
 require "sup"
 
+$exceptions = []
 $opts = Trollop::options do
   version "sup v#{Redwood::VERSION}"
   banner <<EOS
 
   Index.usual_sources.each do |s|
     next unless s.respond_to? :connect
-    reporting_thread do
+    reporting_thread("call #connect on #{s}") do
       begin
         s.connect
       rescue SourceError => e
     end
   end
   
-  imode.load_threads :num => ibuf.content_height, :when_done => lambda { reporting_thread { sleep 1; PollManager.poll } unless $opts[:no_threads] }
+  imode.load_threads :num => ibuf.content_height, :when_done => lambda { reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] }
 
   unless $opts[:no_threads]
     PollManager.start
     SearchResultsMode.spawn_from_query $opts[:search]
   end
 
-  until $exception || SuicideManager.die?
+  until $exceptions.nonempty? || SuicideManager.die?
     c = Ncurses.nonblocking_getch
     next unless c
     bm.erase_flash
       when :list_buffers
         bm.spawn_unless_exists("Buffer List") { BufferListMode.new }
       when :list_contacts
-        b = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
-        b.mode.load_in_background if b
+        b, new = bm.spawn_unless_exists("Contact List") { ContactListMode.new }
+        b.mode.load_in_background if new
       when :search
         query = BufferManager.ask :search, "search all messages: "
         next unless query && query !~ /^\s*$/
       when :compose
         ComposeMode.spawn_nicely
       when :poll
-        reporting_thread { PollManager.poll }
+        reporting_thread("user-invoked poll") { PollManager.poll }
       when :recall_draft
         case Index.num_results_for :label => :draft
         when 0
           BufferManager.spawn "Edit message", r
           r.edit_message
         else
-          b = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
-          b.mode.load_threads :num => b.content_height if b
+          b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
+          b.mode.load_threads :num => b.content_height if new
         end
       when :nothing
       when :redraw
     bm.draw_screen
   end
 rescue Exception => e
-  $exception ||= e
+  $exceptions << [e, "main"]
 ensure
   unless $opts[:no_threads]
     PollManager.stop if PollManager.instantiated?
     Redwood::log "I've been ordered to commit sepuku. I obey!"
   end
 
-  case $exception
-  when nil
+  if $exceptions.empty?
     Redwood::log "no fatal errors. good job, william."
     Index.save
   else
   Index.unlock
 end
 
-if $exception 
+unless $exceptions.empty?
   File.open("sup-exception-log.txt", "w") do |f|
-    f.puts "--- #{e.class.name} at #{Time.now}"
-    f.puts e.message, e.backtrace
+    $exceptions.each do |e, name|
+      f.puts "--- #{e.class.name} from thread: #{name}"
+      f.puts e.message, e.backtrace
+    end
   end
   $stderr.puts <<EOS
 ----------------------------------------------------------------
-I'm very sorry, but it seems that an error occurred in Sup. 
-Please accept my sincere apologies. If you don't mind, please
-send the backtrace below and a brief report of the circumstances
-to sup-talk at rubyforge dot orgs so that I might address this
-problem. Thank you!
+I'm very sorry. It seems that an error occurred in Sup. Please
+accept my sincere apologies. If you don't mind, please send the
+contents of sup-exception-log.txt and a brief report of the
+circumstances to sup-talk at rubyforge dot orgs so that I might
+address this problem. Thank you!
 
 Sincerely,
 William
 ----------------------------------------------------------------
-
-The problem was: '#{$exception.message}' (error type #{$exception.class.name})
-A backtrace follows:
 EOS
-  raise $exception
+  $exceptions.each do |e, name|
+    puts "--- #{e.class.name} from thread: #{name}"
+    puts e.message, e.backtrace
+  end
 end
 
 end
 
 _ mark thread as unread should have a version within thread-view-mode
    which then also closes the buffer
 _ bugfix: time zone parsing broken?
-_ mailing list auto-subscribe/unsubscribe
 _ forwards optionally include attachments
 _ attach messages
 _ flesh out gpg integration: sign & encrypt outgoing
    portion of thread
 _ have "notes" (treated as emails to oneself, never sent) as
    first-class objects.
+x multi-thread dump upon crash
+x hook manager caches values of any proc "variables"
+x bugfix: remove delay on startup if a usual imap source exists
+x bugfix: broken source handling still needs to be improved
+x speed up querying
+x bugfix: sources sometimes aren't added by sup-add
+x more widgets: terminal title, statusbar
+x mailing list auto-subscribe/unsubscribe
 
 future
 ------
+_ ldbd support
+_ don't use a people.txt; store email addresses directly in the index. too many
+  problems with email addresses that occur with multiple names.
+_ infix match instead of prefix match for tab completion (maybe!)
 _ fix killed threads contributing to unread message count problem (prob. need
   to maintain all killed message ids and our own unread message count for
   inbox).
 
     end
 
 ## record exceptions thrown in threads nicely
-  $exception = nil
-  def reporting_thread
+  def reporting_thread name
     if $opts[:no_threads]
       yield
     else
         begin
           yield
         rescue Exception => e
-          $exception ||= e
+          $exceptions ||= []
+          $exceptions << [e, name]
           raise
         end
       end
   end
 
   ## not really a good place for this, so I'll just dump it here.
+  ##
+  ## a source error is either a FatalSourceError or an OutOfSyncSourceError.
+  ## the superclass SourceError is just a generic.
   def report_broken_sources opts={}
     return unless BufferManager.instantiated?
 
-    broken_sources = Index.usual_sources.select { |s| s.error.is_a? FatalSourceError }
-    File.open("goat", "w") { |f| f.puts Kernel.caller }
+    broken_sources = Index.sources.select { |s| s.error.is_a? FatalSourceError }
     unless broken_sources.empty?
       BufferManager.spawn_unless_exists("Broken source notification for #{broken_sources.join(',')}", opts) do
         TextMode.new(<<EOM)
       end
     end
 
-    desynced_sources = Index.usual_sources.select { |s| s.error.is_a? OutOfSyncSourceError }
+    desynced_sources = Index.sources.select { |s| s.error.is_a? OutOfSyncSourceError }
     unless desynced_sources.empty?
       BufferManager.spawn_unless_exists("Out-of-sync source notification for #{broken_sources.join(',')}", opts) do
         TextMode.new(<<EOM)
 
     mode.resize rows, cols
   end
 
-  def redraw
-    draw if @dirty
-    draw_status
+  def redraw status
+    if @dirty
+      draw status 
+    else
+      draw_status status
+    end
+
     commit
   end
 
     @w.noutrefresh
   end
 
-  def draw
+  def draw status
     @mode.draw
-    draw_status
+    draw_status status
     commit
   end
 
     @w.clear
   end
 
-  def draw_status
-    write @height - 1, 0, " [#{mode.name}] #{title}   #{mode.status}",
-      :color => :status_color
+  def draw_status status
+    write @height - 1, 0, status, :color => :status_color
   end
 
   def focus
   ## are canceled by any keypress except this one.
   CONTINUE_IN_BUFFER_SEARCH_KEY = "n"
 
+  HookManager.register "status-bar-text", <<EOS
+Sets the status bar. The default status bar contains the mode name, the buffer
+title, and the mode status. Note that this will be called at least once per
+keystroke, so excessive computation is discouraged.
+
+Variables:
+         num_inbox: number of messages in inbox
+  num_inbox_unread: total number of messages marked as unread
+         num_total: total number of messages in the index
+          num_spam: total number of messages marked as spam
+             title: title of the current buffer
+              mode: current mode name (string)
+            status: current mode status (string)
+Return value: a string to be used as the status bar.
+EOS
+
+  HookManager.register "terminal-title-text", <<EOS
+Sets the title of the current terminal, if applicable. Note that this will be
+called at least once per keystroke, so excessive computation is discouraged.
+
+Variables: the same as status-bar-text hook.
+Return value: a string to be used as the terminal title.
+EOS
+
   def initialize
     @name_map = {}
     @buffers = []
   def completely_redraw_screen
     return if @shelled
 
+    status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
+
     Ncurses.sync do
       @dirty = true
       Ncurses.clear
-      draw_screen :sync => false
+      draw_screen :sync => false, :status => status, :title => title
     end
   end
 
   def draw_screen opts={}
     return if @shelled
 
+    status, title =
+      if opts.member? :status
+        [opts[:status], opts[:title]]
+      else
+        get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
+      end
+
+    print "\033]2;#{title}\07" if title
+
     Ncurses.mutex.lock unless opts[:sync] == false
 
     ## disabling this for the time being, to help with debugging
     false && @buffers.inject(@dirty) do |dirty, buf|
       buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
       #dirty ? buf.draw : buf.redraw
-      buf.draw
+      buf.draw status
       dirty
     end
 
     if true
       buf = @buffers.last
       buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
-      @dirty ? buf.draw : buf.redraw
+      @dirty ? buf.draw(status) : buf.redraw(status)
     end
 
     draw_minibuf :sync => false unless opts[:skip_minibuf]
     Ncurses.mutex.unlock unless opts[:sync] == false
   end
 
-  ## gets the mode from the block, which is only called if the buffer
-  ## doesn't already exist. this is useful in the case that generating
-  ## the mode is expensive, as it often is.
+  ## if the named buffer already exists, pops it to the front without
+  ## calling the block. otherwise, gets the mode from the block and
+  ## creates a new buffer. returns two things: the buffer, and a boolean
+  ## indicating whether it's a new buffer or not.
   def spawn_unless_exists title, opts={}
-    if @name_map.member? title
-      raise_to_front @name_map[title] unless opts[:hidden]
-      nil
-    else
-      mode = yield
-      spawn title, mode, opts
-      @name_map[title]
-    end
+    new = 
+      if @name_map.member? title
+        raise_to_front @name_map[title] unless opts[:hidden]
+        false
+      else
+        mode = yield
+        spawn title, mode, opts
+        true
+      end
+    [@name_map[title], new]
   end
 
   def spawn title, mode, opts={}
   end
 
 private
+  def default_status_bar buf
+    " [#{buf.mode.name}] #{buf.title}   #{buf.mode.status}"
+  end
+
+  def default_terminal_title buf
+    "Sup #{Redwood::VERSION} :: #{buf.title}"
+  end
+
+  def get_status_and_title buf
+    opts = {
+      :num_inbox => lambda { Index.num_results_for :label => :inbox },
+      :num_inbox_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] },
+      :num_total => lambda { Index.size },
+      :num_spam => lambda { Index.num_results_for :label => :spam },
+      :title => buf.title,
+      :mode => buf.mode.name,
+      :status => buf.mode.status
+    }
+
+    statusbar_text = HookManager.run("status-bar-text", opts) || default_status_bar(buf)
+    term_title_text = HookManager.run("terminal-title-text", opts) || default_terminal_title(buf)
+    
+    [statusbar_text, term_title_text]
+  end
 
   def users
     unless @users
 
   ##
   ## i don't bother providing setters, since i'm pretty sure the
   ## charade will fall apart pretty quickly with respect to scoping.
-  ## this is basically fail-fast.
+  ## "fail-fast", we'll call it.
   class HookContext
     def initialize name
       @__say_id = nil
     def method_missing m, *a
       case @__locals[m]
       when Proc
-        @__locals[m].call(*a)
+        @__locals[m] = @__locals[m].call(*a) # only call the proc once
       when nil
         super
       else
     rescue Exception => e
       log "error running hook: #{e.message}"
       log e.backtrace.join("\n")
-      BufferManager.flash "Error running hook: #{e.message}"
       @hooks[name] = nil # disable it
+      BufferManager.flash "Error running hook: #{e.message}"
     end
     context.__cleanup
     result
     end
   end
 
+  def enabled? name; !hook_for(name).nil? end
+
 private
 
   def hook_for name
 
   require 'chronic'
   $have_chronic = true
 rescue LoadError => e
-  Redwood::log "'chronic' library not found. run 'gem install chronic' to install."
+  Redwood::log "optional 'chronic' library not found (run 'gem install chronic' to install)"
   $have_chronic = false
 end
 
   end
 
   def start_lock_update_thread
-    @lock_update_thread = Redwood::reporting_thread do
+    @lock_update_thread = Redwood::reporting_thread("lock update") do
       while true
         sleep 30
         @lock.touch_yourself
   def add_source source
     raise "duplicate source!" if @sources.include? source
     @sources_dirty = true
-    source.id ||= @sources.size
-    ##TODO: why was this necessary?
+    max = @sources.max_of { |id, s| s.is_a?(DraftLoader) || s.is_a?(SentLoader) ? 0 : id }
+    source.id ||= (max || 0) + 1
     ##source.id += 1 while @sources.member? source.id
     @sources[source.id] = source
   end
     end
 
     until pending.empty? || (opts[:limit] && messages.size >= opts[:limit])
-      id = pending.pop
-      next if searched.member? id
-      searched[id] = true
       q = Ferret::Search::BooleanQuery.new true
-      q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should
-      q.add_query Ferret::Search::TermQuery.new(:refs, id), :should
+
+      pending.each do |id|
+        searched[id] = true
+        q.add_query Ferret::Search::TermQuery.new(:message_id, id), :should
+        q.add_query Ferret::Search::TermQuery.new(:refs, id), :should
+      end
+      pending = []
 
       q = build_query :qobj => q
 
           #Redwood::log "got #{mid} as a child of #{id}"
           messages[mid] ||= lambda { build_message docid }
           refs = @index[docid][:refs].split(" ")
-          pending += refs
+          pending += refs.select { |id| !searched[id] }
         end
       end
     end
+
     if killed
       Redwood::log "thread for #{m.id} is killed, ignoring"
       false
 
 ## i would like, for example, to be able to add in a ruby-talk
 ## specific module that would detect and link to /ruby-talk:\d+/
 ## sequences in the text of an email. (how sweet would that be?)
+##
+## this class cathces all source exceptions. if the underlying source throws
+## an error, it is caught and handled.
+
 class Message
   SNIPPET_LEN = 80
   RE_PATTERN = /^((re|re[\[\(]\d[\]\)]):\s*)+/i
           Redwood::log "problem getting messages from #{@source}: #{e.message}"
           ## we need force_to_top here otherwise this window will cover
           ## up the error message one
+          @source.error ||= e
           Redwood::report_broken_sources :force_to_top => true
           [Chunk::Text.new(error_message(e.message))]
         end
 EOS
   end
 
+  ## wrap any source methods that might throw sourceerrors
   def with_source_errors_handled
     begin
       yield
     rescue SourceError => e
       Redwood::log "problem getting messages from #{@source}: #{e.message}"
+      @source.error ||= e
+      Redwood::report_broken_sources :force_to_top => true
       error_message e.message
     end
   end
 
   end
 
   def load_in_background
-    Redwood::reporting_thread do
+    Redwood::reporting_thread("contact manager load in bg") do
       load
       update
       BufferManager.draw_screen
 
   NON_EDITABLE_HEADERS = %w(Message-Id Date)
 
   HookManager.register "signature", <<EOS
-Generates a signature for a message.
+Generates a message signature.
 Variables:
       header: an object that supports string-to-string hashtable-style access
               to the raw headers for the message. E.g., header["From"],
 
     when :inbox
       BufferManager.raise_to_front InboxMode.instance.buffer
     else
-      b = BufferManager.spawn_unless_exists("All threads with label '#{label}'") { LabelSearchResultsMode.new [label] }
-      b.mode.load_threads :num => b.content_height if b
+      b, new = BufferManager.spawn_unless_exists("All threads with label '#{label}'") { LabelSearchResultsMode.new [label] }
+      b.mode.load_threads :num => b.content_height if new
     end
   end
 end
 
     @twiddles = opts.member?(:twiddles) ? opts[:twiddles] : true
     @search_query = nil
     @search_line = nil
+    @status = ""
     super()
   end
 
 
 
   def lines; @text.length; end
   def [] i; @text[i]; end
-  #def contains_thread? t; !@lines[t].nil?; end
   def contains_thread? t; @threads.include?(t) end
 
   def reload
   def select t=nil
     t ||= cursor_thread or return
 
-    ## TODO: don't regen text completely
-    Redwood::reporting_thread do
+    Redwood::reporting_thread("load messages for thread-view-mode") do
       num = t.size
       message = "Loading #{num.pluralize 'message body'}..."
       BufferManager.say(message) do |sid|
 
   def load_n_threads_background n=LOAD_MORE_THREAD_NUM, opts={}
     return if @load_thread # todo: wrap in mutex
-    @load_thread = Redwood::reporting_thread do
+    @load_thread = Redwood::reporting_thread("load threads for thread-index-mode") do
       num = load_n_threads n, opts
       opts[:when_done].call(num) if opts[:when_done]
       @load_thread = nil
 
   def unsubscribe_from_list
     m = @message_lines[curpos] or return
     if m.list_unsubscribe && m.list_unsubscribe =~ /<mailto:(.*?)\?(subject=(.*?))>/
-      spawn_compose_mode :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3
+      ComposeMode.spawn_nicely :from => AccountManager.account_for(m.recipient_email), :to => [PersonManager.person_for($1)], :subj => $3
     else
       BufferManager.flash "Can't find List-Unsubscribe header for this message."
     end
 
   def forward
     m = @message_lines[curpos] or return
-    spawn_forward_mode m
+    ForwardMode.spawn_nicely m
   end
 
   include CanAliasContacts
   def compose
     p = @person_lines[curpos]
     if p
-      spawn_compose_mode :to => [p]
+      ComposeMode.spawn_nicely :to => [p]
     else
-      spawn_compose_mode
+      ComposeMode.spawn_nicely
     end
   end    
 
 
   end
 
   def buffer
-    @buffer ||= BufferManager.spawn_unless_exists("<poll for new messages>", :hidden => true) { PollMode.new }
+    b, new = BufferManager.spawn_unless_exists("<poll for new messages>", :hidden => true) { PollMode.new }
+    b
   end
 
   def poll
   end
 
   def start
-    @thread = Redwood::reporting_thread do
+    @thread = Redwood::reporting_thread("periodic poll") do
       while true
         sleep DELAY / 2
         poll if @last_poll.nil? || (Time.now - @last_poll) >= DELAY
 
 module Redwood
 
-class SourceError < StandardError; end
+class SourceError < StandardError
+  def initialize *a
+    raise "don't instantiate me!" if SourceError.is_a?(self.class)
+    super
+  end
+end
 class OutOfSyncSourceError < SourceError; end
 class FatalSourceError < SourceError; end
 
 
   bool_reader :die
 
   def start
-    @thread = Redwood::reporting_thread do
+    @thread = Redwood::reporting_thread("suicide watch") do
       while true
         sleep DELAY
         if File.exists? @fn
 
   def to_boolean_h; Hash[*map { |x| [x, true] }.flatten]; end
 
   def last= e; self[-1] = e end
+  def nonempty?; !empty? end
 end
 
 class Time
   end
 end
 
-## wraps an object. if it throws an exception, keeps a copy, and
-## rethrows it for any further method calls.
+## wraps an object. if it throws an exception, keeps a copy.
 class Recoverable
   def initialize o
     @o = o
-    @e = nil
+    @error = nil
     @mutex = Mutex.new
   end
 
-  def clear_error!; @e = nil; end
-  def has_errors?; !@e.nil?; end
-  def error; @e; end
+  attr_accessor :error
+
+  def clear_error!; @error = nil; end
+  def has_errors?; !@error.nil?; end
 
   def method_missing m, *a, &b; __pass m, *a, &b end
   
     begin
       @o.send(m, *a, &b)
     rescue Exception => e
-      @e ||= e
-      raise e
+      @error ||= e
+      raise
     end
   end
 end