]> git.cworth.org Git - sup/commitdiff
Merge branch 'buffer-rolling'
authorWilliam Morgan <wmorgan-sup@masanjin.net>
Tue, 25 Aug 2009 13:53:04 +0000 (09:53 -0400)
committerWilliam Morgan <wmorgan-sup@masanjin.net>
Tue, 25 Aug 2009 13:53:04 +0000 (09:53 -0400)
50 files changed:
Manifest.txt
bin/sup
bin/sup-add
bin/sup-config
bin/sup-dump
bin/sup-recover-sources
bin/sup-sync
bin/sup-sync-back
bin/sup-tweak-labels
lib/sup.rb
lib/sup/account.rb
lib/sup/buffer.rb
lib/sup/colormap.rb
lib/sup/contact.rb
lib/sup/crypto.rb
lib/sup/draft.rb
lib/sup/ferret_index.rb
lib/sup/hook.rb
lib/sup/imap.rb
lib/sup/index.rb
lib/sup/interactive-lock.rb [new file with mode: 0644]
lib/sup/label.rb
lib/sup/logger.rb
lib/sup/maildir.rb
lib/sup/mbox.rb
lib/sup/mbox/loader.rb
lib/sup/mbox/ssh-file.rb
lib/sup/message-chunks.rb
lib/sup/message.rb
lib/sup/mode.rb
lib/sup/modes/edit-message-mode.rb
lib/sup/modes/inbox-mode.rb
lib/sup/modes/label-list-mode.rb
lib/sup/modes/log-mode.rb
lib/sup/modes/poll-mode.rb
lib/sup/modes/reply-mode.rb
lib/sup/modes/scroll-mode.rb
lib/sup/modes/text-mode.rb
lib/sup/modes/thread-index-mode.rb
lib/sup/modes/thread-view-mode.rb
lib/sup/poll.rb
lib/sup/sent.rb
lib/sup/source.rb
lib/sup/suicide.rb [deleted file]
lib/sup/textfield.rb
lib/sup/thread.rb
lib/sup/undo.rb
lib/sup/update.rb
lib/sup/util.rb
lib/sup/xapian_index.rb

index be633d776e2b1e5165edeac4d7631cf714471207..09d867eb47595a3ad8a1a7e693734e9f201cb983 100644 (file)
@@ -32,6 +32,7 @@ lib/sup/index.rb
 lib/sup/keymap.rb
 lib/sup/label.rb
 lib/sup/logger.rb
+lib/sup/interactive-lock.rb
 lib/sup/maildir.rb
 lib/sup/mbox.rb
 lib/sup/mbox/loader.rb
@@ -67,7 +68,6 @@ lib/sup/poll.rb
 lib/sup/rfc2047.rb
 lib/sup/sent.rb
 lib/sup/source.rb
-lib/sup/suicide.rb
 lib/sup/tagger.rb
 lib/sup/textfield.rb
 lib/sup/thread.rb
diff --git a/bin/sup b/bin/sup
index 1febefdd5bc0e06e033f6da7a86d1f5bdf2e710f..bbb6c1711a7d5f580530a95859b4853649bf69c9 100755 (executable)
--- a/bin/sup
+++ b/bin/sup
@@ -78,6 +78,7 @@ global_keymap = Keymap.new do |k|
   k.add :compose, "Compose new message", 'm', 'c'
   k.add :nothing, "Do nothing", :ctrl_g
   k.add :recall_draft, "Edit most recent draft message", 'R'
+  k.add :show_inbox, "Show the Inbox buffer", 'I'
 end
 
 ## the following magic enables wide characters when used with a ruby
@@ -95,17 +96,17 @@ module LibC
     else; "libc.so.6"
   end
 
-  Redwood::log "dynamically loading setlocale() from #{setlocale_lib}"
+  debug "dynamically loading setlocale() from #{setlocale_lib}"
   begin
     dlload setlocale_lib
     extern "void setlocale(int, const char *)"
-    Redwood::log "setting locale..."
+    debug "setting locale..."
     LibC.setlocale(6, "")  # LC_ALL == 6
   rescue RuntimeError => e
-    Redwood::log "cannot dlload setlocale(); ncurses wide character support probably broken."
-    Redwood::log "dlload error was #{e.class}: #{e.message}"
+    warn "cannot dlload setlocale(); ncurses wide character support probably broken."
+    warn "dlload error was #{e.class}: #{e.message}"
     if Config::CONFIG['arch'] =~ /bsd/
-      Redwood::log "BSD variant detected. You may have to install a compat6x package to acquire libc."
+      warn "BSD variant detected. You may have to install a compat6x package to acquire libc."
     end
   end
 end
@@ -129,41 +130,21 @@ def stop_cursing
 end
 module_function :start_cursing, :stop_cursing
 
-Index.new
-begin
-  Index.lock
-rescue Index::LockError => e
-  require 'highline'
-
-  h = HighLine.new
-  h.wrap_at = :auto
-  h.say Index.fancy_lock_error_message_for(e)
-
-  case h.ask("Should I ask that process to kill itself? ")
-  when /^\s*y(es)?\s*$/i
-    h.say "Ok, suggesting seppuku..."
-    FileUtils.touch Redwood::SUICIDE_FN
-    sleep SuicideManager::DELAY * 2
-    FileUtils.rm_f Redwood::SUICIDE_FN
-    h.say "Let's try that again."
-    retry
-  else
-    h.say <<EOS
-Ok, giving up. If the process crashed and left a stale lockfile, you
-can fix this by manually deleting #{Index.lockfile}.
-EOS
-    exit
-  end
-end
+Index.init
+Index.lock_interactively or exit
 
 begin
   Redwood::start
   Index.load
 
+  $die = false
+  trap("TERM") { |x| $die = true }
+  trap("WINCH") { |x| BufferManager.sigwinch_happened! }
+
   if(s = Redwood::SourceManager.source_for DraftManager.source_name)
     DraftManager.source = s
   else
-    Redwood::log "no draft source, auto-adding..."
+    debug "no draft source, auto-adding..."
     Redwood::SourceManager.add_source DraftManager.new_source
   end
 
@@ -175,18 +156,24 @@ begin
 
   HookManager.run "startup"
 
-  log "starting curses"
+  debug "starting curses"
+  Redwood::Logger.remove_sink $stderr
   start_cursing
 
-  bm = BufferManager.new
+  bm = BufferManager.init
   Colormap.new.populate_colormap
 
-  log "initializing mail index buffer"
+  debug "initializing log buffer"
+  lmode = Redwood::LogMode.new "system log"
+  lmode.on_kill { Logger.clear! }
+  Logger.add_sink lmode
+  Logger.force_message "Welcome to Sup! Log level is set to #{Logger.level}."
+
+  debug "initializing inbox buffer"
   imode = InboxMode.new
   ibuf = bm.spawn "Inbox", imode
 
-  log "ready for interaction!"
-  Logger.make_buf
+  debug "ready for interaction!"
 
   bm.draw_screen
 
@@ -196,11 +183,11 @@ begin
       begin
         s.connect
       rescue SourceError => e
-        Redwood::log "fatal error loading from #{s}: #{e.message}"
+        error "fatal error loading from #{s}: #{e.message}"
       end
     end
   end unless $opts[:no_initial_poll]
-  
+
   imode.load_threads :num => ibuf.content_height, :when_done => lambda { |num| reporting_thread("poll after loading inbox") { sleep 1; PollManager.poll } unless $opts[:no_threads] || $opts[:no_initial_poll] }
 
   if $opts[:compose]
@@ -209,7 +196,6 @@ begin
 
   unless $opts[:no_threads]
     PollManager.start
-    SuicideManager.start
     Index.start_lock_update_thread
   end
 
@@ -217,30 +203,40 @@ begin
     SearchResultsMode.spawn_from_query $opts[:search]
   end
 
-  until Redwood::exceptions.nonempty? || SuicideManager.die?
-    c = 
-       begin
-         Ncurses.nonblocking_getch
-       rescue Exception => e
-         if e.is_a?(Interrupt)
-           raise if BufferManager.ask_yes_or_no("Die ungracefully now?")
-           bm.draw_screen
-           nil
-         end
-       end
-    next unless c
+  until Redwood::exceptions.nonempty? || $die
+    c = begin
+      Ncurses.nonblocking_getch
+    rescue Interrupt => e
+      raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
+      BufferManager.draw_screen
+      nil
+    end
+
+    if c.nil?
+      if BufferManager.sigwinch_happened?
+        debug "redrawing screen on sigwinch"
+        BufferManager.completely_redraw_screen
+      end
+      next
+    end
+
+    if c == 410
+      ## this is ncurses's way of telling us it's detected a refresh.
+      ## since we have our own sigwinch handler, we don't do anything.
+      next
+    end
+
     bm.erase_flash
 
-    action =
-      begin
-        if bm.handle_input c
-          :nothing
-        else
-          bm.resolve_input_with_keymap c, global_keymap
-        end
-      rescue InputSequenceAborted
+    action = begin
+      if bm.handle_input c
         :nothing
+      else
+        bm.resolve_input_with_keymap c, global_keymap
       end
+    rescue InputSequenceAborted
+      :nothing
+    end
     case action
     when :quit_now
       break if bm.kill_all_buffers_safely
@@ -296,6 +292,8 @@ begin
         b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
         b.mode.load_threads :num => b.content_height if new
       end
+    when :show_inbox
+      BufferManager.raise_to_front ibuf
     when :nothing, InputSequenceAborted
     when :redraw
       bm.completely_redraw_screen
@@ -306,13 +304,12 @@ begin
     bm.draw_screen
   end
 
-  bm.kill_all_buffers if SuicideManager.die?
+  bm.kill_all_buffers if $die
 rescue Exception => e
   Redwood::record_exception e, "main"
 ensure
   unless $opts[:no_threads]
     PollManager.stop if PollManager.instantiated?
-    SuicideManager.stop if PollManager.instantiated?
     Index.stop_lock_update_thread
   end
 
@@ -320,17 +317,19 @@ ensure
 
   Redwood::finish
   stop_cursing
-  Redwood::log "stopped cursing"
+  Redwood::Logger.remove_all_sinks!
+  Redwood::Logger.add_sink $stderr, false
+  debug "stopped cursing"
 
-  if SuicideManager.instantiated? && SuicideManager.die?
-    Redwood::log "I've been ordered to commit seppuku. I obey!"
+  if $die
+    info "I've been ordered to commit seppuku. I obey!"
   end
 
   if Redwood::exceptions.empty?
-    Redwood::log "no fatal errors. good job, william."
+    debug "no fatal errors. good job, william."
     Index.save
   else
-    Redwood::log "oh crap, an exception"
+    error "oh crap, an exception"
   end
 
   Index.unlock
index 3ab7c4db0a18bf4bdb07bf2b9b7f9af375172e78..e27a0ebf6ff446a9347d4fd8937653ab45b97db8 100755 (executable)
@@ -77,9 +77,9 @@ end
 
 $terminal.wrap_at = :auto
 Redwood::start
-index = Redwood::Index.new
+index = Redwood::Index.init
 
-index.lock_or_die
+index.lock_interactively or exit
 
 begin
   Redwood::SourceManager.load_sources
index 9fcbee60e527250700b6aaaf0e64a4746b22dda2..bd55fc1825fad84b6cf6cc8c0185d52effbc5adf 100755 (executable)
@@ -151,7 +151,7 @@ end
 
 $terminal.wrap_at = :auto
 Redwood::start
-index = Redwood::Index.new
+index = Redwood::Index.init
 Redwood::SourceManager.load_sources
 
 say <<EOS
index ba36b21adb0f102342f370d417640785541890bb..8b5bf07f07be990e49ffdb96ff90b623e69a1a9a 100755 (executable)
@@ -21,10 +21,10 @@ No options.
 EOS
 end
 
-index = Redwood::Index.new
-Redwood::SourceManager.new
+index = Redwood::Index.init
+Redwood::SourceManager.init
 index.load
 
 index.each_message :load_spam => true, :load_deleted => true, :load_killed => true do |m|
-  puts "#{m.id} (#{m.labels * ' '})"
+  puts "#{m.id} (#{m.labels.to_a * ' '})"
 end
index db75b11a392f8043346d7af4769d6a6244ec3b2b..43fa5f640f0fcd862ceac622388a471a8573edc2 100755 (executable)
@@ -50,7 +50,7 @@ end.parse(ARGV)
 require "sup"
 Redwood::start
 puts "loading index..."
-index = Redwood::Index.new
+index = Redwood::Index.init
 index.load
 puts "loaded index of #{index.size} messages"
 
index 44ff3b20a873f01dc33fde8c7b6345521cc84fb9..2aa00c3720bcd0dbbce60499e5d14a70e01ae418 100755 (executable)
@@ -9,9 +9,7 @@ PROGRESS_UPDATE_INTERVAL = 15 # seconds
 
 class Float
   def to_s; sprintf '%.2f', self; end
-   def to_time_s
-     infinite? ? "unknown" : super
-   end
+  def to_time_s; infinite? ? "unknown" : super end
 end
 
 class Numeric
@@ -21,6 +19,10 @@ class Numeric
   end
 end
 
+class Set
+  def to_s; to_a * ',' end
+end
+
 def time
   startt = Time.now
   yield
@@ -54,7 +56,7 @@ by running "sup-add --help".
 Options controlling WHICH messages sup-sync operates on:
 EOS
   opt :new, "Operate on new messages only. Don't scan over the entire source. (Default.)", :short => :none
-  opt :changed, "Scan over the entire source for messages that have been deleted, altered, or moved from another source. (In the case of mbox sources, this includes all messages AFTER an altered message.)"
+  opt :changed, "Scan over the entire source for messages that have been deleted, altered, or moved from another source."
   opt :restored, "Operate only on those messages included in a dump file as specified by --restore which have changed state."
   opt :all, "Operate on all messages in the source, regardless of newness or changedness."
   opt :start_at, "For --changed, --restored and --all, start at a particular offset.", :type => :int
@@ -68,7 +70,7 @@ EOS
   opt :discard, "Discard any message state in the index and use the default source state. Dangerous!", :short => :none
   opt :archive, "When using the default source state, mark messages as archived.", :short => "-x"
   opt :read, "When using the default source state, mark messages as read."
-  opt :extra_labels, "When using the default source state, also apply these user-defined labels. Should be a comma-separated list.", :type => String, :short => :none
+  opt :extra_labels, "When using the default source state, also apply these user-defined labels (a comma-separated list)", :default => "", :short => :none
 
 text <<EOS
 
@@ -93,35 +95,39 @@ target = [:new, :changed, :all, :restored].find { |x| opts[x] } || :new
 op = [:asis, :restore, :discard].find { |x| opts[x] } || :asis
 
 Redwood::start
-index = Redwood::Index.new
-
-restored_state =
-  if opts[:restore]
-    dump = {}
-    $stderr.puts "Loading state dump from #{opts[:restore]}..."
-    IO.foreach opts[:restore] do |l|
-      l =~ /^(\S+) \((.*?)\)$/ or raise "Can't read dump line: #{l.inspect}"
-      mid, labels = $1, $2
-      dump[mid] = labels.symbolistize
-    end
-    $stderr.puts "Read #{dump.size} entries from dump file."
-    dump
-  else
-    {}
+index = Redwood::Index.init
+
+restored_state = if opts[:restore]
+  dump = {}
+  $stderr.puts "Loading state dump from #{opts[:restore]}..."
+  IO.foreach opts[:restore] do |l|
+    l =~ /^(\S+) \((.*?)\)$/ or raise "Can't read dump line: #{l.inspect}"
+    mid, labels = $1, $2
+    dump[mid] = labels.to_set_of_symbols
   end
+  $stderr.puts "Read #{dump.size} entries from dump file."
+  dump
+else
+  {}
+end
 
 seen = {}
-index.lock_or_die
+index.lock_interactively or exit
 begin
   index.load
 
-  sources = ARGV.map do |uri|
-    Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
+  sources = if opts[:all_sources]
+    Redwood::SourceManager.sources
+  elsif ARGV.empty?
+    Redwood::SourceManager.usual_sources
+  else
+    ARGV.map do |uri|
+      Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
+    end
   end
-  
-  sources = Redwood::SourceManager.usual_sources if sources.empty?
-  sources = Redwood::SourceManager.sources if opts[:all_sources]
 
+  ## for all target specifications except for only-new messages, reset the
+  ## source to the beginning (or to the user-specified starting point.)
   unless target == :new
     if opts[:start_at]
       Trollop::die :start_at, "can only be used on one source" unless sources.size == 1
@@ -131,57 +137,79 @@ begin
       sources.each { |s| s.reset! }
     end
   end
-  
+
   sources.each do |source|
     $stderr.puts "Scanning #{source}..."
     num_added = num_updated = num_scanned = num_restored = 0
     last_info_time = start_time = Time.now
 
-    Redwood::PollManager.add_messages_from source, :force_overwrite => true do |m_old, m, offset|
+    Redwood::PollManager.each_message_from source do |m|
       num_scanned += 1
       seen[m.id] = true
-
-      if Time.now - last_info_time > PROGRESS_UPDATE_INTERVAL
-        last_info_time = Time.now
-        elapsed = last_info_time - start_time
-        start = opts[:start_at] || source.start_offset
-        pctdone = 100.0 * (source.cur_offset - start).to_f / (source.end_offset - start).to_f
-        remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
-        $stderr.printf "## read %dm (about %.0f%%) @ %.1fm/s. %s elapsed, about %s remaining\n", num_scanned, pctdone, num_scanned / elapsed, elapsed.to_time_s, remaining.to_time_s
+      old_m = index.build_message m.id
+
+      case target
+      when :changed
+        ## skip this message if we're operating only on changed messages, the
+        ## message is in the index, and it's unchanged from what the source is
+        ## reporting.
+        next if old_m && old_m.source.id == m.source.id && old_m.source_info == m.source_info
+      when :restored
+        ## skip if we're operating on restored messages, and this one
+        ## ain't (or we wouldn't be making a change)
+        next unless old_m && restored_state[m.id] && restored_state[m.id] != old_m.labels
+      when :new
+        ## nothing to do; we'll consider all messages starting at the start offset, which
+        ## hasn't been changed.
+      when :all
+        ## nothing to do; we'll consider all messages starting at the start offset, which
+        ## was reset to the beginning above.
       end
 
-      ## skip if we're operating only on changed messages, the message
-      ## is in the index, and it's unchanged from what the source is
-      ## reporting.
-      next if target == :changed && m_old && m_old.source.id == source.id && m_old.source_info == offset
-
-      ## get the state currently in the index
-      index_state = m_old.labels.dup if m_old
-
-      ## skip if we're operating on restored messages, and this one
-      ## ain't.
-      next if target == :restored && (!restored_state[m.id] || (index_state && restored_state[m.id].sort_by { |s| s.to_s } == index_state.sort_by { |s| s.to_s }))
-
-      ## m.labels is the default source labels. tweak these according
-      ## to default source state modification flags.
-      m.labels -= [:inbox] if opts[:archive]
-      m.labels -= [:unread] if opts[:read]
-      m.labels += opts[:extra_labels].strip.split(/\s*,\s*/).map { |x| x.intern } if opts[:extra_labels]
-
-      ## assign message labels based on the operation we're performing
-      case op
-      when :asis
-        m.labels = ((m.labels - [:unread, :inbox]) + index_state).uniq if index_state
-      when :restore
-        ## if the entry exists on disk
-        if restored_state[m.id]
-          m.labels = restored_state[m.id]
-          num_restored += 1
-        elsif index_state
-          m.labels = index_state
+      ## tweak source labels according to commandline arguments if necessary
+      m.labels.delete :inbox if opts[:archive]
+      m.labels.delete :unread if opts[:read]
+      m.labels += opts[:extra_labels].to_set_of_symbols(",")
+
+      ## decide what to do based on message labels and the operation we're performing
+      dothis, new_labels = case
+      when (op == :restore) && restored_state[m.id] && old_m && (old_m.labels != restored_state[m.id])
+        [:update_message_state, restored_state[m.id]]
+      when op == :discard
+        if old_m && (old_m.labels != m.labels)
+          [:update_message_state, m.labels]
+        else
+          # don't do anything
         end
-      when :discard
-        ## nothin! use default source labels
+      else
+        ## duplicate behavior of poll mode: if index_state is non-nil, this is a newer
+        ## version of an older message, so merge in any new labels except :unread and
+        ## :inbox.
+        ##
+        ## TODO: refactor such that this isn't duplicated
+        if old_m
+          m.labels = old_m.labels + (m.labels - [:unread, :inbox])
+          :update_message
+        else
+          :add_message
+        end
+      end
+
+      ## now, actually do the operation
+      case dothis
+      when :add_message
+        $stderr.puts "Adding new message #{source}###{m.source_info} with labels #{m.labels}" if opts[:verbose]
+        index.add_message m unless opts[:dry_run]
+        num_added += 1
+      when :update_message
+        $stderr.puts "Updating message #{source}###{m.source_info}; labels #{old_m.labels} => #{m.labels}; offset #{old_m.source_info} => #{m.source_info}" if opts[:verbose]
+        index.update_message m unless opts[:dry_run]
+        num_updated += 1
+      when :update_message_state
+        $stderr.puts "Changing flags for #{source}##{m.source_info} from #{m.labels} to #{new_labels}"
+        m.labels = new_labels
+        index.update_message_state m unless opts[:dry_run]
+        num_updated += 1
       end
 
       if Time.now - last_info_time > PROGRESS_UPDATE_INTERVAL
@@ -191,17 +219,8 @@ begin
         remaining = (100.0 - pctdone) * (elapsed.to_f / pctdone)
         $stderr.printf "## read %dm (about %.0f%%) @ %.1fm/s. %s elapsed, about %s remaining\n", num_scanned, pctdone, num_scanned / elapsed, elapsed.to_time_s, remaining.to_time_s
       end
-
-      if index_state.nil?
-        puts "Adding message #{source}##{offset} from #{m.from} with state {#{m.labels * ', '}}" if opts[:verbose]
-        num_added += 1
-      else
-        puts "Updating message #{source}##{offset}, source #{m_old.source.id} => #{source.id}, offset #{m_old.source_info} => #{offset}, state {#{index_state * ', '}} => {#{m.labels * ', '}}" if opts[:verbose]
-        num_updated += 1
-      end
-
-      opts[:dry_run] ? nil : m
     end
+
     $stderr.puts "Scanned #{num_scanned}, added #{num_added}, updated #{num_updated} messages from #{source}."
     $stderr.puts "Restored state on #{num_restored} (#{100.0 * num_restored / num_scanned}%) messages." if num_restored > 0
   end
index 56ac4eb51c05545e8879c976fcbf8b9c4d695c65..6298c97c3e6190e8704ff2cd1333843f09511fea 100755 (executable)
@@ -65,8 +65,8 @@ EOS
 end
 
 Redwood::start
-index = Redwood::Index.new
-index.lock_or_die
+index = Redwood::Index.init
+index.lock_interactively or exit
 
 deleted_fp, spam_fp = nil
 unless opts[:dry_run]
@@ -108,30 +108,30 @@ EOS
 
     source.reset!
     num_dropped = num_moved = num_scanned = 0
-    
+
     out_fp = Tempfile.new "sup-sync-back-#{source.id}"
-    Redwood::PollManager.add_messages_from source do |m_old, m, offset|
+    Redwood::PollManager.each_message_from source do |m|
       num_scanned += 1
 
-      if m_old
+      if(m_old = index.build_message(m.id))
         labels = m_old.labels
 
         if labels.member? :deleted
           if opts[:drop_deleted]
-            puts "Dropping deleted message #{source}##{offset}" if opts[:verbose]
+            puts "Dropping deleted message #{source}##{m.source_info}" if opts[:verbose]
             num_dropped += 1
           elsif opts[:move_deleted] && labels.member?(:deleted)
-            puts "Moving deleted message #{source}##{offset}" if opts[:verbose]
+            puts "Moving deleted message #{source}##{m.source_info}" if opts[:verbose]
             save m, deleted_fp unless opts[:dry_run]
             num_moved += 1
           end
 
         elsif labels.member? :spam
           if opts[:drop_spam]
-            puts "Dropping spam message #{source}##{offset}" if opts[:verbose]
+            puts "Dropping spam message #{source}##{m.source_info}" if opts[:verbose]
             num_dropped += 1
           elsif opts[:move_spam] && labels.member?(:spam)
-            puts "Moving spam message #{source}##{offset}" if opts[:verbose]
+            puts "Moving spam message #{source}##{m.source_info}" if opts[:verbose]
             save m, spam_fp unless opts[:dry_run]
             num_moved += 1
           end
@@ -141,8 +141,6 @@ EOS
       else
         save m, out_fp unless opts[:dry_run]
       end
-
-      nil # don't actually add anything!
     end
     $stderr.puts "Scanned #{num_scanned}, dropped #{num_dropped}, moved #{num_moved} messages from #{source}."
     modified_sources << source if num_dropped > 0 || num_moved > 0
index 8ae5c26f9054c841ad612bdaf29c79f300cab753..90f6a57ddb0cbebd037f4a74dd1692a191de522e 100755 (executable)
@@ -38,8 +38,8 @@ be seen by running "sup-add --help".
 
 Options:
 EOS
-  opt :add, "One or more labels (comma-separated) to add to every message from the specified sources", :type => String
-  opt :remove, "One or more labels (comma-separated) to remove from every message from the specified sources, if those labels are present", :type => String
+  opt :add, "One or more labels (comma-separated) to add to every message from the specified sources", :default => ""
+  opt :remove, "One or more labels (comma-separated) to remove from every message from the specified sources, if those labels are present", :default => ""
   opt :query, "A Sup search query", :type => String
 
   text <<EOS
@@ -54,23 +54,24 @@ EOS
 end
 opts[:verbose] = true if opts[:very_verbose]
 
-add_labels = (opts[:add] || "").split(",").map { |l| l.intern }.uniq
-remove_labels = (opts[:remove] || "").split(",").map { |l| l.intern }.uniq
+add_labels = opts[:add].to_set_of_symbols ","
+remove_labels = opts[:remove].to_set_of_symbols ","
 
 Trollop::die "nothing to do: no labels to add or remove" if add_labels.empty? && remove_labels.empty?
+Trollop::die "no sources specified" if ARGV.empty?
 
 Redwood::start
+index = Redwood::Index.init
+index.lock_interactively or exit
 begin
-  index = Redwood::Index.new
   index.load
 
-  source_ids = 
-    if opts[:all_sources]
-      Redwood::SourceManager.sources
-    else
-      ARGV.map do |uri|
-        Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
-      end
+  source_ids = if opts[:all_sources]
+    Redwood::SourceManager.sources
+  else
+    ARGV.map do |uri|
+      Redwood::SourceManager.source_for uri or Trollop::die "Unknown source: #{uri}. Did you add it with sup-add first?"
+    end
   end.map { |s| s.id }
   Trollop::die "nothing to do: no sources" if source_ids.empty?
 
@@ -95,18 +96,17 @@ begin
     num_scanned += 1
 
     m = index.build_message id
-    old_labels = m.labels.clone
+    old_labels = m.labels.dup
 
     m.labels += add_labels
     m.labels -= remove_labels
-    m.labels = m.labels.uniq
 
-    unless m.labels.sort_by { |s| s.to_s } == old_labels.sort_by { |s| s.to_s }
+    unless m.labels == old_labels
       num_changed += 1
       puts "From #{m.from}, subject: #{m.subj}" if opts[:very_verbose]
-      puts "#{m.id}: {#{old_labels.join ','}} => {#{m.labels.join ','}}" if opts[:verbose]
+      puts "#{m.id}: {#{old_labels.to_a.join ','}} => {#{m.labels.to_a.join ','}}" if opts[:verbose]
       puts if opts[:very_verbose]
-      index.sync_message m unless opts[:dry_run]
+      index.update_message_state m unless opts[:dry_run]
     end
 
     if Time.now - last_info_time > 60
index 54de73f5597f1e2f752f07a00694d39de547880b..16c2b3f3bbf802d6e713714a48aecbde41ad8dc2 100644 (file)
@@ -24,7 +24,7 @@ class Module
     vars = props.map { |p| "@#{p}" }
     klass = self
     path = klass.name.gsub(/::/, "/")
-    
+
     klass.instance_eval do
       define_method(:to_yaml_properties) { vars }
       define_method(:to_yaml_type) { "!#{Redwood::YAML_DOMAIN},#{Redwood::YAML_DATE}/#{path}" }
@@ -85,39 +85,50 @@ module Redwood
   module_function :reporting_thread, :record_exception, :exceptions
 
 ## one-stop shop for yamliciousness
-  def save_yaml_obj object, fn, safe=false
+  def save_yaml_obj o, fn, safe=false
+    o = if o.is_a?(Array)
+      o.map { |x| (x.respond_to?(:before_marshal) && x.before_marshal) || x }
+    else
+      o.respond_to?(:before_marshal) && o.before_marshal
+    end
+
     if safe
       safe_fn = "#{File.dirname fn}/safe_#{File.basename fn}"
       mode = File.stat(fn).mode if File.exists? fn
-      File.open(safe_fn, "w", mode) { |f| f.puts object.to_yaml }
+      File.open(safe_fn, "w", mode) { |f| f.puts o.to_yaml }
       FileUtils.mv safe_fn, fn
     else
-      File.open(fn, "w") { |f| f.puts object.to_yaml }
+      File.open(fn, "w") { |f| f.puts o.to_yaml }
     end
   end
 
   def load_yaml_obj fn, compress=false
-    if File.exists? fn
+    o = if File.exists? fn
       if compress
         Zlib::GzipReader.open(fn) { |f| YAML::load f }
       else
         YAML::load_file fn
       end
     end
+    if o.is_a?(Array)
+      o.each { |x| x.after_unmarshal! if x.respond_to?(:after_unmarshal!) }
+    else
+      o.after_unmarshal! if o.respond_to?(:after_unmarshal!)
+    end
+    o
   end
 
   def start
-    Redwood::SentManager.new $config[:sent_source] || 'sup://sent'
-    Redwood::ContactManager.new Redwood::CONTACT_FN
-    Redwood::LabelManager.new Redwood::LABEL_FN
-    Redwood::AccountManager.new $config[:accounts]
-    Redwood::DraftManager.new Redwood::DRAFT_DIR
-    Redwood::UpdateManager.new
-    Redwood::PollManager.new
-    Redwood::SuicideManager.new Redwood::SUICIDE_FN
-    Redwood::CryptoManager.new
-    Redwood::UndoManager.new
-    Redwood::SourceManager.new
+    Redwood::SentManager.init $config[:sent_source] || 'sup://sent'
+    Redwood::ContactManager.init Redwood::CONTACT_FN
+    Redwood::LabelManager.init Redwood::LABEL_FN
+    Redwood::AccountManager.init $config[:accounts]
+    Redwood::DraftManager.init Redwood::DRAFT_DIR
+    Redwood::UpdateManager.init
+    Redwood::PollManager.init
+    Redwood::CryptoManager.init
+    Redwood::UndoManager.init
+    Redwood::SourceManager.init
   end
 
   def finish
@@ -230,33 +241,29 @@ require "sup/hook"
 ## we have to initialize this guy first, because other classes must
 ## reference it in order to register hooks, and they do that at parse
 ## time.
-Redwood::HookManager.new Redwood::HOOK_DIR
+Redwood::HookManager.init Redwood::HOOK_DIR
 
 ## everything we need to get logging working
-require "sup/buffer"
-require "sup/keymap"
-require "sup/mode"
-require "sup/modes/scroll-mode"
-require "sup/modes/text-mode"
-require "sup/modes/log-mode"
 require "sup/logger"
-module Redwood
-  def log s; Logger.log s; end
-  module_function :log
-end
+Redwood::Logger.init.add_sink $stderr
+include Redwood::LogsStuff
 
 ## determine encoding and character set
   $encoding = Locale.current.charset
   if $encoding
-    Redwood::log "using character set encoding #{$encoding.inspect}"
+    debug "using character set encoding #{$encoding.inspect}"
   else
-    Redwood::log "warning: can't find character set by using locale, defaulting to utf-8"
+    warn "can't find character set by using locale, defaulting to utf-8"
     $encoding = "UTF-8"
   end
 
-## now everything else (which can feel free to call Redwood::log at load time)
+require "sup/buffer"
+require "sup/keymap"
+require "sup/mode"
+require "sup/modes/scroll-mode"
+require "sup/modes/text-mode"
+require "sup/modes/log-mode"
 require "sup/update"
-require "sup/suicide"
 require "sup/message-chunks"
 require "sup/message"
 require "sup/source"
@@ -266,6 +273,7 @@ require "sup/imap"
 require "sup/person"
 require "sup/account"
 require "sup/thread"
+require "sup/interactive-lock"
 require "sup/index"
 require "sup/textfield"
 require "sup/colormap"
index 6f86129ca5d8997b0572f38512ba84a691ee4a7c..eed2794d5f8562647a52f18686f2f635963d5799 100644 (file)
@@ -25,8 +25,6 @@ class AccountManager
 
     add_account accounts[:default], true
     accounts.each { |k, v| add_account v, false unless k == :default }
-
-    self.class.i_am_the_instance self
   end
 
   def user_accounts; @accounts.keys; end
index 77a0e1e4685a2e87bb7695247ea50c3a8dd7c17a..d85090a6db57eab56a8e230a6aeb8e3a925e6339 100644 (file)
@@ -25,13 +25,13 @@ module Ncurses
   def mutex; @mutex ||= Mutex.new; end
   def sync &b; mutex.synchronize(&b); end
 
-  ## magically, this stuff seems to work now. i could swear it didn't
-  ## before. hm.
   def nonblocking_getch
-    if IO.select([$stdin], nil, nil, 1)
-      Ncurses.getch
-    else
-      nil
+    ## INSANTIY
+    ## it is NECESSARY to wrap Ncurses.getch in a select() otherwise all
+    ## background threads will be BLOCKED. (except in very modern versions
+    ## of libncurses-ruby. the current one on ubuntu seems to work well.)
+    if IO.select([$stdin], nil, nil, 0.5)
+      c = Ncurses.getch
     end
   end
 
@@ -70,7 +70,7 @@ class Buffer
   def content_height; @height - 1; end
   def content_width; @width; end
 
-  def resize rows, cols 
+  def resize rows, cols
     return if cols == @width && rows == @height
     @width = cols
     @height = rows
@@ -196,10 +196,13 @@ EOS
     @flash = nil
     @shelled = @asking = false
     @in_x = ENV["TERM"] =~ /(xterm|rxvt|screen)/
-
-    self.class.i_am_the_instance self
+    @sigwinch_happened = false
+    @sigwinch_mutex = Mutex.new
   end
 
+  def sigwinch_happened!; @sigwinch_mutex.synchronize { @sigwinch_happened = true } end
+  def sigwinch_happened?; @sigwinch_mutex.synchronize { @sigwinch_happened } end
+
   def buffers; @name_map.to_a; end
 
   def focus_on buf
@@ -267,6 +270,14 @@ EOS
   def completely_redraw_screen
     return if @shelled
 
+    ## this magic makes Ncurses get the new size of the screen
+    Ncurses.endwin
+    Ncurses.stdscr.keypad 1
+    Ncurses.curs_set 0
+    Ncurses.refresh
+    @sigwinch_mutex.synchronize { @sigwinch_happened = false }
+    debug "new screen size is #{Ncurses.rows} x #{Ncurses.cols}"
+
     status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
 
     Ncurses.sync do
@@ -474,7 +485,7 @@ EOS
     end
 
     if answer
-      answer = 
+      answer =
         if answer.empty?
           spawn_modal "file browser", FileBrowserMode.new
         elsif File.directory?(answer)
@@ -490,7 +501,7 @@ EOS
   ## returns an array of labels
   def ask_for_labels domain, question, default_labels, forbidden_labels=[]
     default_labels = default_labels - forbidden_labels - LabelManager::RESERVED_LABELS
-    default = default_labels.join(" ")
+    default = default_labels.to_a.join(" ")
     default += " " unless default.empty?
 
     # here I would prefer to give more control and allow all_labels instead of
@@ -501,7 +512,7 @@ EOS
 
     return unless answer
 
-    user_labels = answer.symbolistize
+    user_labels = answer.to_set_of_symbols
     user_labels.each do |l|
       if forbidden_labels.include?(l) || LabelManager::RESERVED_LABELS.include?(l)
         BufferManager.flash "'#{l}' is a reserved label!"
@@ -514,7 +525,7 @@ EOS
   def ask_for_contacts domain, question, default_contacts=[]
     default = default_contacts.map { |s| s.to_s }.join(" ")
     default += " " unless default.empty?
-    
+
     recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
     contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
 
index 38787cddc41374a9f9adf583160627a5dfb448e0..fbbbfc9e9405d1be3eb88d4dd647b6e96e9e85d8 100644 (file)
@@ -129,7 +129,7 @@ class Colormap
       @next_id = (@next_id + 1) % NUM_COLORS
       @next_id += 1 if @next_id == 0 # 0 is always white on black
       id = @next_id
-      Redwood::log "colormap: for color #{sym}, using id #{id} -> #{fg}, #{bg}"
+      debug "colormap: for color #{sym}, using id #{id} -> #{fg}, #{bg}"
       Curses.init_pair id, fg, bg or raise ArgumentError,
         "couldn't initialize curses color pair #{fg}, #{bg} (key #{id})"
 
@@ -137,7 +137,7 @@ class Colormap
       ## delete the old mapping, if it exists
       if @users[cp]
         @users[cp].each do |usym|
-          Redwood::log "dropping color #{usym} (#{id})"
+          warn "dropping color #{usym} (#{id})"
           @entries[usym][3] = nil
         end
         @users[cp] = []
@@ -155,7 +155,7 @@ class Colormap
   ## to the default ones.
   def populate_colormap
     user_colors = if File.exists? Redwood::COLOR_FN
-      Redwood::log "loading user colors from #{Redwood::COLOR_FN}"
+      debug "loading user colors from #{Redwood::COLOR_FN}"
       Redwood::load_yaml_obj Redwood::COLOR_FN
     end
 
@@ -171,7 +171,7 @@ class Colormap
             fg = Curses.const_get "COLOR_#{ufg.upcase}"
           rescue NameError
             error ||= "Warning: there is no color named \"#{ufg}\", using fallback."
-            Redwood::log "Warning: there is no color named \"#{ufg}\""
+            warn "there is no color named \"#{ufg}\""
           end
         end
 
@@ -180,7 +180,7 @@ class Colormap
             bg = Curses.const_get "COLOR_#{ubg.upcase}"
           rescue NameError
             error ||= "Warning: there is no color named \"#{ubg}\", using fallback."
-            Redwood::log "Warning: there is no color named \"#{ubg}\""
+            warn "there is no color named \"#{ubg}\""
           end
         end
 
@@ -190,7 +190,7 @@ class Colormap
               Curses.const_get "A_#{a.upcase}"
             rescue NameError
               error ||= "Warning: there is no attribute named \"#{a}\", using fallback."
-              Redwood::log "Warning: there is no attribute named \"#{a}\", using fallback."
+              warn "there is no attribute named \"#{a}\", using fallback."
             end
           end
         end
index 51fd0e978c902f17485c8fcd81f20cc766b74157..c489aaff5a4d67ce9d7156a10013f329bf790f11 100644 (file)
@@ -22,8 +22,6 @@ class ContactManager
         @a2p[aalias] = p unless aalias.nil? || aalias.empty?
       end
     end
-
-    self.class.i_am_the_instance self
   end
 
   def contacts; @p2a.keys end
index 8ec277b862603fb23d94a60d3fa19fd60ee0fd0f..7f044b99428d0beb80bfb22fa1f4cda6bc127011 100644 (file)
@@ -13,17 +13,15 @@ class CryptoManager
 
   def initialize
     @mutex = Mutex.new
-    self.class.i_am_the_instance self
 
     bin = `which gpg`.chomp
-
     @cmd =
       case bin
       when /\S/
-        Redwood::log "crypto: detected gpg binary in #{bin}"
+        debug "crypto: detected gpg binary in #{bin}"
         "#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
       else
-        Redwood::log "crypto: no gpg binary detected"
+        debug "crypto: no gpg binary detected"
         nil
       end
   end
@@ -116,21 +114,19 @@ class CryptoManager
     output = run_gpg "--decrypt #{payload_fn.path}"
 
     if $?.success?
-      decrypted_payload, sig_lines =
-        if output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
-          [$1, $2]
+      decrypted_payload, sig_lines = if output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
+        [$1, $2]
+      else
+        [output, nil]
+      end
+
+      sig = if sig_lines # encrypted & signed
+        if sig_lines =~ /^gpg: (Good signature from .*$)/
+          Chunk::CryptoNotice.new :valid, $1, sig_lines.split("\n")
         else
-          [output, nil]
-        end
-      
-      sig = 
-        if sig_lines # encrypted & signed
-          if sig_lines =~ /^gpg: (Good signature from .*$)/
-            Chunk::CryptoNotice.new :valid, $1, sig_lines.split("\n")
-          else
-            Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
-          end
+          Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
         end
+      end
 
       notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
       [RMail::Parser.read(decrypted_payload), sig, notice]
@@ -145,7 +141,7 @@ private
   def unknown_status lines=[]
     Chunk::CryptoNotice.new :unknown, "Unable to determine validity of cryptographic signature", lines
   end
-  
+
   def cant_find_binary
     ["Can't find gpg binary in path."]
   end
@@ -158,9 +154,7 @@ private
 
   def run_gpg args
     cmd = "#{@cmd} #{args} 2> /dev/null"
-    #Redwood::log "crypto: running: #{cmd}"
     output = `#{cmd}`
-    #Redwood::log "crypto: output: #{output.inspect}" unless $?.success?
     output
   end
 end
index dd4574da7db9aa010989c2b9353932e89688d9ac..5ea2935f2180f3cb8a94ac996a50ce97dcc6d1ac 100644 (file)
@@ -7,7 +7,6 @@ class DraftManager
   def initialize dir
     @dir = dir
     @source = nil
-    self.class.i_am_the_instance self
   end
 
   def self.source_name; "sup://drafts"; end
@@ -20,12 +19,9 @@ class DraftManager
     File.open(fn, "w") { |f| yield f }
 
     my_message = nil
-    @source.each do |thisoffset, theselabels|
-      m = Message.build_from_source @source, thisoffset
-      m.labels = theselabels
-      Index.sync_message m
-      UpdateManager.relay self, :added, m
-      my_message = m if thisoffset == offset
+    PollManager.each_message_from(@source) do |m|
+      PollManager.add_new_message m
+      my_message = m
     end
 
     my_message
index f3d414770212e090d0ce03ff9b58a5a07df45233..df1139d97b67d878fa84f9440a173206f11cf26f 100644 (file)
@@ -18,13 +18,13 @@ class FerretIndex < BaseIndex
 
   def load_index dir=File.join(@dir, "ferret")
     if File.exists? dir
-      Redwood::log "loading index..."
+      debug "loading index..."
       @index_mutex.synchronize do
         @index = Ferret::Index::Index.new(:path => dir, :analyzer => @analyzer, :id_field => 'message_id')
-        Redwood::log "loaded index of #{@index.size} messages"
+        debug "loaded index of #{@index.size} messages"
       end
     else
-      Redwood::log "creating index..."
+      debug "creating index..."
       @index_mutex.synchronize do
         field_infos = Ferret::Index::FieldInfos.new :store => :yes
         field_infos.add_field :message_id, :index => :untokenized
@@ -45,6 +45,10 @@ class FerretIndex < BaseIndex
     end
   end
 
+  def add_message m; sync_message m end
+  def update_message m; sync_message m end
+  def update_message_state m; sync_message m end
+
   def sync_message m, opts={}
     entry = @index[m.id]
 
@@ -79,16 +83,16 @@ class FerretIndex < BaseIndex
     ## written in this manner to support previous versions of the index which
     ## did not keep around the entry body. upgrading is thus seamless.
     entry ||= {}
-    labels = m.labels.uniq # override because this is the new state, unless...
+    labels = m.labels # override because this is the new state, unless...
 
     ## if we are a later version of a message, ignore what's in the index,
     ## but merge in the labels.
     if entry[:source_id] && entry[:source_info] && entry[:label] &&
       ((entry[:source_id].to_i > source_id) || (entry[:source_info].to_i < m.source_info))
-      labels = (entry[:label].symbolistize + m.labels).uniq
-      #Redwood::log "found updated version of message #{m.id}: #{m.subj}"
-      #Redwood::log "previous version was at #{entry[:source_id].inspect}:#{entry[:source_info].inspect}, this version at #{source_id.inspect}:#{m.source_info.inspect}"
-      #Redwood::log "merged labels are #{labels.inspect} (index #{entry[:label].inspect}, message #{m.labels.inspect})"
+      labels += entry[:label].to_set_of_symbols
+      #debug "found updated version of message #{m.id}: #{m.subj}"
+      #debug "previous version was at #{entry[:source_id].inspect}:#{entry[:source_info].inspect}, this version at #{source_id.inspect}:#{m.source_info.inspect}"
+      #debug "merged labels are #{labels.inspect} (index #{entry[:label].inspect}, message #{m.labels.inspect})"
       entry = {}
     end
 
@@ -103,7 +107,7 @@ class FerretIndex < BaseIndex
       :date => (entry[:date] || m.date.to_indexable_s),
       :body => (entry[:body] || m.indexable_content),
       :snippet => snippet, # always override
-      :label => labels.uniq.join(" "),
+      :label => labels.to_a.join(" "),
       :attachments => (entry[:attachments] || m.attachments.uniq.join(" ")),
 
       ## always override :from and :to.
@@ -125,6 +129,7 @@ class FerretIndex < BaseIndex
       @index.add_document d
     end
   end
+  private :sync_message
 
   def save_index fn=File.join(@dir, "ferret")
     # don't have to do anything, apparently
@@ -146,7 +151,7 @@ class FerretIndex < BaseIndex
     while true
       limit = (query[:limit])? [EACH_BY_DATE_NUM, query[:limit] - offset].min : EACH_BY_DATE_NUM
       results = @index_mutex.synchronize { @index.search ferret_query, :sort => "date DESC", :limit => limit, :offset => offset }
-      Redwood::log "got #{results.total_hits} results for query (offset #{offset}) #{ferret_query.inspect}"
+      debug "got #{results.total_hits} results for query (offset #{offset}) #{ferret_query.inspect}"
       results.hits.each do |hit|
         yield @index_mutex.synchronize { @index[hit.doc][:message_id] }, lambda { build_message hit.doc }
       end
@@ -165,7 +170,7 @@ class FerretIndex < BaseIndex
   SAME_SUBJECT_DATE_LIMIT = 7
   MAX_CLAUSES = 1000
   def each_message_in_thread_for m, opts={}
-    #Redwood::log "Building thread for #{m.id}: #{m.subj}"
+    #debug "Building thread for #{m.id}: #{m.subj}"
     messages = {}
     searched = {}
     num_queries = 0
@@ -186,10 +191,10 @@ class FerretIndex < BaseIndex
       q = build_ferret_query :qobj => q
 
       p1 = @index_mutex.synchronize { @index.search(q).hits.map { |hit| @index[hit.doc][:message_id] } }
-      Redwood::log "found #{p1.size} results for subject query #{q}"
+      debug "found #{p1.size} results for subject query #{q}"
 
       p2 = @index_mutex.synchronize { @index.search(q.to_s, :limit => :all).hits.map { |hit| @index[hit.doc][:message_id] } }
-      Redwood::log "found #{p2.size} results in string form"
+      debug "found #{p2.size} results in string form"
 
       pending = (pending + p1 + p2).uniq
     end
@@ -220,7 +225,7 @@ class FerretIndex < BaseIndex
           end
           mid = @index[docid][:message_id]
           unless messages.member?(mid)
-            #Redwood::log "got #{mid} as a child of #{id}"
+            #debug "got #{mid} as a child of #{id}"
             messages[mid] ||= lambda { build_message docid }
             refs = @index[docid][:refs].split
             pending += refs.select { |id| !searched[id] }
@@ -230,10 +235,10 @@ class FerretIndex < BaseIndex
     end
 
     if killed
-      #Redwood::log "thread for #{m.id} is killed, ignoring"
+      #debug "thread for #{m.id} is killed, ignoring"
       false
     else
-      #Redwood::log "ran #{num_queries} queries to build thread of #{messages.size} messages for #{m.id}: #{m.subj}" if num_queries > 0
+      #debug "ran #{num_queries} queries to build thread of #{messages.size} messages for #{m.id}: #{m.subj}" if num_queries > 0
       messages.each { |mid, builder| yield mid, builder }
       true
     end
@@ -259,7 +264,7 @@ class FerretIndex < BaseIndex
       }
 
       m = Message.new :source => source, :source_info => doc[:source_info].to_i,
-                  :labels => doc[:label].symbolistize,
+                  :labels => doc[:label].to_set_of_symbols,
                   :snippet => doc[:snippet]
       m.parse_header fake_header
       m
@@ -280,13 +285,13 @@ class FerretIndex < BaseIndex
     end
     q.add_query Ferret::Search::TermQuery.new(:label, "spam"), :must_not
 
-    Redwood::log "contact search: #{q}"
+    debug "contact search: #{q}"
     contacts = {}
     num = h[:num] || 20
     @index_mutex.synchronize do
       @index.search_each q, :sort => "date DESC", :limit => :all do |docid, score|
         break if contacts.size >= num
-        #Redwood::log "got message #{docid} to: #{@index[docid][:to].inspect} and from: #{@index[docid][:from].inspect}"
+        #debug "got message #{docid} to: #{@index[docid][:to].inspect} and from: #{@index[docid][:from].inspect}"
         f = @index[docid][:from]
         t = @index[docid][:to]
 
@@ -376,10 +381,10 @@ class FerretIndex < BaseIndex
       field, name = $1, ($3 || $4)
       case field
       when "filename"
-        Redwood::log "filename - translated #{field}:#{name} to attachments:(#{name.downcase})"
+        debug "filename: translated #{field}:#{name} to attachments:(#{name.downcase})"
         "attachments:(#{name.downcase})"
       when "filetype"
-        Redwood::log "filetype - translated #{field}:#{name} to attachments:(*.#{name.downcase})"
+        debug "filetype: translated #{field}:#{name} to attachments:(*.#{name.downcase})"
         "attachments:(*.#{name.downcase})"
       end
     end
@@ -391,13 +396,13 @@ class FerretIndex < BaseIndex
         if realdate
           case field
           when "after"
-            Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate.end}"
+            debug "chronic: translated #{field}:#{datestr} to #{realdate.end}"
             "date:(>= #{sprintf "%012d", realdate.end.to_i})"
           when "before"
-            Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate.begin}"
+            debug "chronic: translated #{field}:#{datestr} to #{realdate.begin}"
             "date:(<= #{sprintf "%012d", realdate.begin.to_i})"
           else
-            Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate}"
+            debug "chronic: translated #{field}:#{datestr} to #{realdate}"
             "date:(<= #{sprintf "%012d", realdate.end.to_i}) date:(>= #{sprintf "%012d", realdate.begin.to_i})"
           end
         else
index 0a0a2f672fca775e4bb792896537d2ec2535fac3..0c411626c43d1ad31e94493860f919b2ebf631e5 100644 (file)
@@ -40,7 +40,7 @@ class HookManager
     end
 
     def log s
-      Redwood::log "hook[#@__name]: #{s}"
+      info "hook[#@__name]: #{s}"
     end
 
     def ask_yes_or_no q
@@ -79,8 +79,6 @@ class HookManager
     @tags = {}
 
     Dir.mkdir dir unless File.exists? dir
-
-    self.class.i_am_the_instance self
   end
 
   attr_reader :tags
@@ -148,7 +146,7 @@ private
   end
 
   def log m
-    Redwood::log("hook: " + m)
+    info("hook: " + m)
   end
 end
 
index 6c04d885d5856a719ba46460b465f51186034a3e..bdb9e15b52fec1971b7e705ffa5e0ac6988e5446 100644 (file)
@@ -4,6 +4,7 @@ require 'stringio'
 require 'time'
 require 'rmail'
 require 'cgi'
+require 'set'
 
 ## TODO: remove synchronized method protector calls; use a Monitor instead
 ## (ruby's reentrant mutex)
@@ -47,6 +48,7 @@ require 'cgi'
 module Redwood
 
 class IMAP < Source
+  include SerializeLabelsNicely
   SCAN_INTERVAL = 60 # seconds
 
   ## upon these errors we'll try to rereconnect a few times
@@ -69,7 +71,7 @@ class IMAP < Source
     @imap_state = {}
     @ids = []
     @last_scan = nil
-    @labels = ((labels || []) - LabelManager::RESERVED_LABELS).uniq.freeze
+    @labels = Set.new((labels || []) - LabelManager::RESERVED_LABELS)
     @say_id = nil
     @mutex = Mutex.new
   end
@@ -159,13 +161,13 @@ class IMAP < Source
     return if last_id == @ids.length
 
     range = (@ids.length + 1) .. last_id
-    Redwood::log "fetching IMAP headers #{range}"
+    debug "fetching IMAP headers #{range}"
     fetch(range, ['RFC822.SIZE', 'INTERNALDATE', 'FLAGS']).each do |v|
       id = make_id v
       @ids << id
       @imap_state[id] = { :id => v.seqno, :flags => v.attr["FLAGS"] }
     end
-    Redwood::log "done fetching IMAP headers"
+    debug "done fetching IMAP headers"
   end
   synchronized :scan_mailbox
 
@@ -224,7 +226,7 @@ private
     if good_results.empty?
       raise FatalSourceError, "no IMAP response for #{ids} containing all fields #{fields.join(', ')} (got #{results.size} results)"
     elsif good_results.size < results.size
-      Redwood::log "Your IMAP server sucks. It sent #{results.size} results for a request for #{good_results.size} messages. What are you using, Binc?"
+      warn "Your IMAP server sucks. It sent #{results.size} results for a request for #{good_results.size} messages. What are you using, Binc?"
     end
 
     good_results
@@ -252,12 +254,12 @@ private
           raise Net::IMAP::NoResponseError unless @imap.capability().member? "AUTH=CRAM-MD5"
           @imap.authenticate 'CRAM-MD5', @username, @password
         rescue Net::IMAP::BadResponseError, Net::IMAP::NoResponseError => e
-          Redwood::log "CRAM-MD5 authentication failed: #{e.class}. Trying LOGIN auth..."
+          debug "CRAM-MD5 authentication failed: #{e.class}. Trying LOGIN auth..."
           begin
             raise Net::IMAP::NoResponseError unless @imap.capability().member? "AUTH=LOGIN"
             @imap.authenticate 'LOGIN', @username, @password
           rescue Net::IMAP::BadResponseError, Net::IMAP::NoResponseError => e
-            Redwood::log "LOGIN authentication failed: #{e.class}. Trying plain-text LOGIN..."
+            debug "LOGIN authentication failed: #{e.class}. Trying plain-text LOGIN..."
             @imap.login @username, @password
           end
         end
@@ -274,7 +276,7 @@ private
 
   def say s
     @say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
-    Redwood::log s
+    info s
   end
 
   def shutup
@@ -331,7 +333,7 @@ private
       rescue *RECOVERABLE_ERRORS => e
         if (retries += 1) <= 3
           @imap = nil
-          Redwood::log "got #{e.class.name}: #{e.message.inspect}"
+          warn "got #{e.class.name}: #{e.message.inspect}"
           sleep 2
           retry
         end
index fb46eb053a02b871923e93281021441414e11683..ff03f195f6bca74aefdb23eedecc01c2172ee39c 100644 (file)
@@ -6,13 +6,15 @@ begin
   require 'chronic'
   $have_chronic = true
 rescue LoadError => e
-  Redwood::log "optional 'chronic' library not found (run 'gem install chronic' to install)"
+  debug "optional 'chronic' library not found; date-time query restrictions disabled"
   $have_chronic = false
 end
 
 module Redwood
 
 class BaseIndex
+  include InteractiveLock
+
   class LockError < StandardError
     def initialize h
       @h = h
@@ -26,13 +28,12 @@ class BaseIndex
   def initialize dir=BASE_DIR
     @dir = dir
     @lock = Lockfile.new lockfile, :retries => 0, :max_age => nil
-    self.class.i_am_the_instance self
   end
 
   def lockfile; File.join @dir, "lock" end
 
   def lock
-    Redwood::log "locking #{lockfile}..."
+    debug "locking #{lockfile}..."
     begin
       @lock.lock
     rescue Lockfile::MaxTriesLockError
@@ -54,45 +55,9 @@ class BaseIndex
     @lock_update_thread = nil
   end
 
-  def possibly_pluralize number_of, kind
-    "#{number_of} #{kind}" +
-        if number_of == 1 then "" else "s" end
-  end
-
-  def fancy_lock_error_message_for e
-    secs = (Time.now - e.mtime).to_i
-    mins = secs / 60
-    time =
-      if mins == 0
-        possibly_pluralize secs , "second"
-      else
-        possibly_pluralize mins, "minute"
-      end
-
-    <<EOS
-Error: the sup index is locked by another process! User '#{e.user}' on
-host '#{e.host}' is running #{e.pname} with pid #{e.pid}. The process was alive
-as of #{time} ago.
-EOS
-  end
-
-  def lock_or_die
-    begin
-      lock
-    rescue LockError => e
-      $stderr.puts fancy_lock_error_message_for(e)
-      $stderr.puts <<EOS
-
-You can wait for the process to finish, or, if it crashed and left a
-stale lock file behind, you can manually delete #{@lock.path}.
-EOS
-      exit
-    end
-  end
-
   def unlock
     if @lock && @lock.locked?
-      Redwood::log "unlocking #{lockfile}..."
+      debug "unlocking #{lockfile}..."
       @lock.unlock
     end
   end
@@ -103,7 +68,7 @@ EOS
   end
 
   def save
-    Redwood::log "saving index and sources..."
+    debug "saving index and sources..."
     FileUtils.mkdir_p @dir unless File.exists? @dir
     SourceManager.save_sources
     save_index
@@ -113,12 +78,9 @@ EOS
     unimplemented
   end
 
-  ## Syncs the message to the index, replacing any previous version.  adding
-  ## either way. Index state will be determined by the message's #labels
-  ## accessor.
-  def sync_message m, opts={}
-    unimplemented
-  end
+  def add_message m; unimplemented end
+  def update_message m; unimplemented end
+  def update_message_state m; unimplemented end
 
   def save_index fn
     unimplemented
@@ -219,6 +181,6 @@ case index_name
   else fail "unknown index type #{index_name.inspect}"
 end
 Index = Redwood.const_get "#{index_name.capitalize}Index"
-Redwood::log "using index #{Index.name}"
+debug "using index #{Index.name}"
 
 end
diff --git a/lib/sup/interactive-lock.rb b/lib/sup/interactive-lock.rb
new file mode 100644 (file)
index 0000000..92a5ead
--- /dev/null
@@ -0,0 +1,74 @@
+require 'fileutils'
+
+module Redwood
+
+## wrap a nice interactive layer on top of anything that has a #lock method
+## which throws a LockError which responds to #user, #host, #mtim, #pname, and
+## #pid.
+
+module InteractiveLock
+  def pluralize number_of, kind; "#{number_of} #{kind}" + (number_of == 1 ? "" : "s") end
+
+  def time_ago_in_words time
+    secs = (Time.now - time).to_i
+    mins = secs / 60
+    time = if mins == 0
+      pluralize secs, "second"
+    else
+      pluralize mins, "minute"
+    end
+  end
+
+  DELAY = 5 # seconds
+
+  def lock_interactively stream=$stderr
+    begin
+      Index.lock
+    rescue Index::LockError => e
+      stream.puts <<EOS
+Error: the index is locked by another process! User '#{e.user}' on
+host '#{e.host}' is running #{e.pname} with pid #{e.pid}.
+The process was alive as of at least #{time_ago_in_words e.mtime} ago.
+
+EOS
+      stream.print "Should I ask that process to kill itself (y/n)? "
+      stream.flush
+
+      success = if $stdin.gets =~ /^\s*y(es)?\s*$/i
+        stream.puts "Ok, trying to kill process..."
+
+        begin
+          Process.kill "TERM", e.pid.to_i
+          sleep DELAY
+        rescue Errno::ESRCH # no such process
+          stream.puts "Hm, I couldn't kill it."
+        end
+
+        stream.puts "Let's try that again."
+        begin
+          Index.lock
+        rescue Index::LockError => e
+          stream.puts "I couldn't lock the index. The lockfile might just be stale."
+          stream.print "Should I just remove it and continue? (y/n) "
+          stream.flush
+
+          if $stdin.gets =~ /^\s*y(es)?\s*$/i
+            FileUtils.rm e.path
+
+            stream.puts "Let's try that one more time."
+            begin
+              Index.lock
+              true
+            rescue Index::LockError => e
+            end
+          end
+        end
+      end
+
+      stream.puts "Sorry, couldn't unlock the index." unless success
+      success
+    end
+  end
+end
+
+end
index 47d632baafaa58e2dfa10111172b64e08b9f9f55..67474c2b79ed3393aa3321e458fdb4675225dfe1 100644 (file)
@@ -22,8 +22,6 @@ class LabelManager
     @new_labels = {}
     @modified = false
     labels.each { |t| @labels[t] = true }
-
-    self.class.i_am_the_instance self
   end
 
   def new_label? l; @new_labels.include?(l) end
@@ -61,9 +59,9 @@ class LabelManager
       l
     end
   end
-  
+
   def << t
-    t = t.intern unless t.is_a? Symbol
+    raise ArgumentError, "expecting a symbol" unless t.is_a? Symbol
     unless @labels.member?(t) || RESERVED_LABELS.member?(t)
       @labels[t] = true
       @new_labels[t] = true
index 4ac6551bff21e50ab45704f295d6d930edf1ce6d..ccaeae0c0728f15c08898a948f70b64b547f708b 100644 (file)
@@ -1,54 +1,73 @@
+require "sup"
+require 'stringio'
+require 'thread'
+
 module Redwood
 
+## simple centralized logger. outputs to multiple sinks by calling << on them.
+## also keeps a record of all messages, so that adding a new sink will send all
+## previous messages to it by default.
 class Logger
-  @@instance = nil
+  include Singleton
 
-  attr_reader :buf
+  LEVELS = %w(debug info warn error) # in order!
 
-  def initialize
-    raise "only one Log can be defined" if @@instance
-    @@instance = self
-    @mode = LogMode.new
-    @respawn = true
-    @spawning = false # to prevent infinite loops!
+  def initialize level=nil
+    level ||= ENV["SUP_LOG_LEVEL"] || "info"
+    @level = LEVELS.index(level) or raise ArgumentError, "invalid log level #{level.inspect}: should be one of #{LEVELS * ', '}"
+    @mutex = Mutex.new
+    @buf = StringIO.new
+    @sinks = []
   end
 
-  ## must be called if you want to see anything!
-  ## once called, will respawn if killed...
-  def make_buf
-    return if @mode.buffer || !BufferManager.instantiated? || !@respawn || @spawning
-    @spawning = true
-    @mode.buffer = BufferManager.instance.spawn "log", @mode, :hidden => true, :system => true
-    @spawning = false
+  def level; LEVELS[@level] end
+
+  def add_sink s, copy_current=true
+    @mutex.synchronize do
+      @sinks << s
+      s << @buf.string if copy_current
+    end
   end
 
-  def log s
-#    $stderr.puts s
-    make_buf
-    prefix = "#{Time.now}: "
-    padding = " " * prefix.length
-    first = true
-    s.split(/[\r\n]/).each do |l|
-      l = l.chomp
-      if first
-        first = false
-        @mode << "#{prefix}#{l}\n"
-      else
-        @mode << "#{padding}#{l}\n"
+  def remove_sink s; @mutex.synchronize { @sinks.delete s } end
+  def remove_all_sinks!; @mutex.synchronize { @sinks.clear } end
+  def clear!; @mutex.synchronize { @buf = StringIO.new } end
+
+  LEVELS.each_with_index do |l, method_level|
+    define_method(l) do |s|
+      if method_level >= @level
+        send_message format_message(l, Time.now, s)
       end
     end
-    $stderr.puts "[#{Time.now}] #{s.chomp}" unless BufferManager.instantiated? && @mode.buffer
   end
-  
-  def self.method_missing m, *a
-    @@instance = Logger.new unless @@instance
-    @@instance.send m, *a
+
+  ## send a message regardless of the current logging level
+  def force_message m; send_message format_message(nil, Time.now, m) end
+
+private
+
+  ## level can be nil!
+  def format_message level, time, msg
+    prefix = case level
+      when "warn"; "WARNING: "
+      when "error"; "ERROR: "
+      else ""
+    end
+    "[#{time.to_s}] #{prefix}#{msg}\n"
   end
 
-  def self.buffer
-    @@instance.buf
+  ## actually distribute the message
+  def send_message m
+    @mutex.synchronize do
+      @sinks.each { |sink| sink << m }
+      @buf << m
+    end
   end
 end
 
+## include me to have top-level #debug, #info, etc. methods.
+module LogsStuff
+  Logger::LEVELS.each { |l| define_method(l) { |s| Logger.instance.send(l, s) } }
 end
 
+end
index a2dbae460f187a2d483b9307d032d47030df654e..2c33e3bdc06b08f68b1d4b3caf564021eb6d71b8 100644 (file)
@@ -9,6 +9,7 @@ module Redwood
 ## pathnames on disk.
 
 class Maildir < Source
+  include SerializeLabelsNicely
   SCAN_INTERVAL = 30 # seconds
   MYHOSTNAME = Socket.gethostname
 
@@ -23,7 +24,7 @@ class Maildir < Source
     raise ArgumentError, "maildir URI must have a path component" unless uri.path
 
     @dir = uri.path
-    @labels = (labels || []).freeze
+    @labels = Set.new(labels || [])
     @ids = []
     @ids_to_fns = {}
     @last_scan = nil
@@ -115,7 +116,7 @@ class Maildir < Source
 
     initial_poll = @ids.empty?
 
-    Redwood::log "scanning maildir #@dir..."
+    debug "scanning maildir #@dir..."
     begin
       @mtimes.each_key do |d|
        subdir = File.join(@dir, d)
@@ -134,7 +135,7 @@ class Maildir < Source
            @ids_to_fns[id] = fn
          end
        else
-         Redwood::log "no poll on #{d}.  mtime on indicates no new messages."
+         debug "no poll on #{d}.  mtime on indicates no new messages."
        end
       end
       @ids = @dir_ids.values.flatten.uniq.sort!
@@ -142,7 +143,7 @@ class Maildir < Source
       raise FatalSourceError, "Problem scanning Maildir directories: #{e.message}."
     end
     
-    Redwood::log "done scanning maildir"
+    debug "done scanning maildir"
     @last_scan = Time.now
   end
   synchronized :scan_mailbox
index e1e3a4d1aed75c0a3d190cdc9ce8257ff46a56e4..3f3abadefeb6f3833970abf027e6e8f77ce7dd45 100644 (file)
@@ -15,7 +15,7 @@ module MBox
       Time.parse time, 0
       true
     rescue NoMethodError
-      Redwood::log "found invalid date in potential mbox split line, not splitting: #{l.inspect}"
+      warn "found invalid date in potential mbox split line, not splitting: #{l.inspect}"
       false
     end
   end
index ea277cf0f80ffa41397484a92a6940cb468dd0a2..030759483289f2ee6bd115b818e9dfecb5a986de 100644 (file)
@@ -1,17 +1,18 @@
 require 'rmail'
 require 'uri'
+require 'set'
 
 module Redwood
 module MBox
 
 class Loader < Source
+  include SerializeLabelsNicely
   yaml_properties :uri, :cur_offset, :usual, :archived, :id, :labels
-  attr_accessor :labels
 
   ## uri_or_fp is horrific. need to refactor.
-  def initialize uri_or_fp, start_offset=0, usual=true, archived=false, id=nil, labels=[]
+  def initialize uri_or_fp, start_offset=0, usual=true, archived=false, id=nil, labels=nil
     @mutex = Mutex.new
-    @labels = ((labels || []) - LabelManager::RESERVED_LABELS).uniq.freeze
+    @labels = Set.new((labels || []) - LabelManager::RESERVED_LABELS)
 
     case uri_or_fp
     when String
@@ -47,7 +48,7 @@ class Loader < Source
       raise OutOfSyncSourceError, "mbox file is smaller than last recorded message offset. Messages have probably been deleted by another client."
     end
   end
-    
+
   def start_offset; 0; end
   def end_offset; File.size @f; end
 
@@ -168,7 +169,7 @@ class Loader < Source
     end
 
     self.cur_offset = next_offset
-    [returned_offset, (self.labels + [:unread]).uniq]
+    [returned_offset, (@labels + [:unread])]
   end
 end
 
index d47463692e07bedce205e4c3527f87f345c528c6..4ae4bbadf6f875b8655dbe5b461bca2b31afef22 100644 (file)
@@ -16,12 +16,6 @@ class SSHFileError < StandardError; end
 ## all of the methods here can throw SSHFileErrors, SocketErrors,
 ## Net::SSH::Exceptions and Errno::ENOENTs.
 
-## debugging TODO: remove me
-def debug s
-  Redwood::log s
-end
-module_function :debug
-
 ## a simple buffer of contiguous data
 class Buffer
   def initialize
@@ -154,7 +148,7 @@ private
   ## TODO: share this code with imap
   def say s
     @say_id = BufferManager.say s, @say_id if BufferManager.instantiated?
-    Redwood::log s
+    info s
   end
 
   def shutup
index 0d742d99e746a9ce96a4c11eddcd57506fcae22f..1eda17438df45e08eaa6f5269fca073e1714bb19 100644 (file)
@@ -132,7 +132,7 @@ EOS
     def viewable?; @lines.nil? end
     def view_default! path
       cmd = "/usr/bin/run-mailcap --action=view '#{@content_type}:#{path}' 2>/dev/null"
-      Redwood::log "running: #{cmd.inspect}"
+      debug "running: #{cmd.inspect}"
       system cmd
       $? == 0
     end
index 7c6374651e379ea0947caf7072390bba6d981eb3..ed27d3dccfba056410bd90225604479f8e788797 100644 (file)
@@ -46,7 +46,7 @@ class Message
     @snippet = opts[:snippet]
     @snippet_contains_encrypted_content = false
     @have_snippet = !(opts[:snippet].nil? || opts[:snippet].empty?)
-    @labels = (opts[:labels] || []).to_set_of_symbols
+    @labels = Set.new(opts[:labels] || [])
     @dirty = false
     @encrypted = false
     @chunks = nil
@@ -73,7 +73,7 @@ class Message
     else
       id = "sup-faked-" + Digest::MD5.hexdigest(raw_header)
       from = header["from"]
-      #Redwood::log "faking non-existent message-id for message from #{from}: #{id}"
+      #debug "faking non-existent message-id for message from #{from}: #{id}"
       id
     end
 
@@ -81,7 +81,7 @@ class Message
       header["from"]
     else
       name = "Sup Auto-generated Fake Sender <sup@fake.sender.example.com>"
-      #Redwood::log "faking non-existent sender for message #@id: #{name}"
+      #debug "faking non-existent sender for message #@id: #{name}"
       name
     end)
 
@@ -92,11 +92,11 @@ class Message
       begin
         Time.parse date
       rescue ArgumentError => e
-        #Redwood::log "faking mangled date header for #{@id} (orig #{header['date'].inspect} gave error: #{e.message})"
+        #debug "faking mangled date header for #{@id} (orig #{header['date'].inspect} gave error: #{e.message})"
         Time.now
       end
     else
-      #Redwood::log "faking non-existent date header for #{@id}"
+      #debug "faking non-existent date header for #{@id}"
       Time.now
     end
 
@@ -157,22 +157,22 @@ class Message
   ## don't tempt me.
   def sanitize_message_id mid; mid.gsub(/(\s|[^\000-\177])+/, "")[0..254] end
 
-  def save index
+  def save_state index
     return unless @dirty
-    index.sync_message self
+    index.update_message_state self
     @dirty = false
     true
   end
 
   def has_label? t; @labels.member? t; end
-  def add_label t
-    return if @labels.member? t
-    @labels = (@labels + [t]).to_set_of_symbols
+  def add_label l
+    return if @labels.member? l
+    @labels << l
     @dirty = true
   end
-  def remove_label t
-    return unless @labels.member? t
-    @labels.delete t
+  def remove_label l
+    return unless @labels.member? l
+    @labels.delete l
     @dirty = true
   end
 
@@ -181,7 +181,9 @@ class Message
   end
 
   def labels= l
-    @labels = l.to_set_of_symbols
+    raise ArgumentError, "not a set" unless l.is_a?(Set)
+    return if @labels == l
+    @labels = l
     @dirty = true
   end
 
@@ -208,7 +210,7 @@ class Message
           parse_header @source.load_header(@source_info)
           message_to_chunks @source.load_message(@source_info)
         rescue SourceError, SocketError => e
-          Redwood::log "problem getting messages from #{@source}: #{e.message}"
+          warn "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
@@ -242,7 +244,7 @@ EOS
     begin
       yield
     rescue SourceError => e
-      Redwood::log "problem getting messages from #{@source}: #{e.message}"
+      warn "problem getting messages from #{@source}: #{e.message}"
       @source.error ||= e
       Redwood::report_broken_sources :force_to_top => true
       error_message e.message
@@ -333,25 +335,25 @@ private
 
   def multipart_signed_to_chunks m
     if m.body.size != 2
-      Redwood::log "warning: multipart/signed with #{m.body.size} parts (expecting 2)"
+      warn "multipart/signed with #{m.body.size} parts (expecting 2)"
       return
     end
 
     payload, signature = m.body
     if signature.multipart?
-      Redwood::log "warning: multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
+      warn "multipart/signed with payload multipart #{payload.multipart?} and signature multipart #{signature.multipart?}"
       return
     end
 
     ## this probably will never happen
     if payload.header.content_type == "application/pgp-signature"
-      Redwood::log "warning: multipart/signed with payload content type #{payload.header.content_type}"
+      warn "multipart/signed with payload content type #{payload.header.content_type}"
       return
     end
 
     if signature.header.content_type != "application/pgp-signature"
       ## unknown signature type; just ignore.
-      #Redwood::log "warning: multipart/signed with signature content type #{signature.header.content_type}"
+      #warn "multipart/signed with signature content type #{signature.header.content_type}"
       return
     end
 
@@ -360,23 +362,23 @@ private
 
   def multipart_encrypted_to_chunks m
     if m.body.size != 2
-      Redwood::log "warning: multipart/encrypted with #{m.body.size} parts (expecting 2)"
+      warn "multipart/encrypted with #{m.body.size} parts (expecting 2)"
       return
     end
 
     control, payload = m.body
     if control.multipart?
-      Redwood::log "warning: multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
+      warn "multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
       return
     end
 
     if payload.header.content_type != "application/octet-stream"
-      Redwood::log "warning: multipart/encrypted with payload content type #{payload.header.content_type}"
+      warn "multipart/encrypted with payload content type #{payload.header.content_type}"
       return
     end
 
     if control.header.content_type != "application/pgp-encrypted"
-      Redwood::log "warning: multipart/encrypted with control content type #{signature.header.content_type}"
+      warn "multipart/encrypted with control content type #{signature.header.content_type}"
       return
     end
 
index 6433492c4d50acb187c1ae426620fde6a13b36be..209ca45e44bb36333159627335bd64dd7f8b50ea 100644 (file)
@@ -92,10 +92,10 @@ EOS
       unless err.empty?
         message = err.first.read
         if message =~ /^\s*$/
-          Redwood::log "error running #{command} (but no error message)"
+          warn "error running #{command} (but no error message)"
           BufferManager.flash "Error running #{command}!"
         else
-          Redwood::log "error running #{command}: #{message}"
+          warn "error running #{command}: #{message}"
           BufferManager.flash "Error: #{message}"
         end
         return
index a48930aea521fd73d7c83f2bf9846d8be6a29025..8da316fe00593f8085adb167e0200b5a6c650d07 100644 (file)
@@ -325,7 +325,7 @@ protected
       BufferManager.flash "Message sent!"
       true
     rescue SystemCallError, SendmailCommandFailed, CryptoManager::Error => e
-      Redwood::log "Problem sending mail: #{e.message}"
+      warn "Problem sending mail: #{e.message}"
       BufferManager.flash "Problem sending mail: #{e.message}"
       false
     end
index d8daeb9c4cbe19ad1fc2644aa1d5c84a4e382f23..ba095dab7c09696f60efb7e68c993d614da9e7b8 100644 (file)
@@ -1,4 +1,4 @@
-require 'thread'
+require 'sup'
 
 module Redwood
 
@@ -15,9 +15,7 @@ class InboxMode < ThreadIndexMode
     @@instance = self
   end
 
-  def is_relevant? m
-    m.has_label?(:inbox) && ([:spam, :deleted, :killed] & m.labels).empty?
-  end
+  def is_relevant? m; (m.labels & [:spam, :deleted, :killed, :inbox]) == Set.new([:inbox]) end
 
   ## label-list-mode wants to be able to raise us if the user selects
   ## the "inbox" label, so we need to keep our singletonness around
index 53287c1156611dc835ab87c571a27b52d7c092b6..f65ec2e2080f2f823b1fa102fb77d2cc5f4cbae5 100644 (file)
@@ -73,7 +73,7 @@ protected
       ##   TODO make the labelmanager responsible for label counts
       ## and then it can listen to labeled and unlabeled events, etc.
       if total == 0 && !LabelManager::RESERVED_LABELS.include?(label) && !LabelManager.new_label?(label)
-        Redwood::log "no hits for label #{label}, deleting"
+        debug "no hits for label #{label}, deleting"
         LabelManager.delete label
         next
       end
index de16b5e6635b988fa90329b7d66ef046d3793372..07fa9dd6cda8d4780676119839ee053baee41560 100644 (file)
@@ -1,36 +1,40 @@
+require 'stringio'
 module Redwood
 
+## a variant of text mode that allows the user to automatically follow text,
+## and respawns when << is called if necessary.
+
 class LogMode < TextMode
   register_keymap do |k|
     k.add :toggle_follow, "Toggle follow mode", 'f'
   end
 
-  def initialize
+  def initialize buffer_name
     @follow = true
-    super
+    @buffer_name = buffer_name
+    @on_kill = []
+    super()
   end
 
+  ## register callbacks for when the buffer is killed
+  def on_kill &b; @on_kill << b end
+
   def toggle_follow
     @follow = !@follow
-    if buffer
-      if @follow
-        jump_to_line lines - buffer.content_height + 1 # leave an empty line at bottom
-      end
-      buffer.mark_dirty
+    if @follow
+      jump_to_line(lines - buffer.content_height + 1) # leave an empty line at bottom
     end
+    buffer.mark_dirty
   end
 
-  def text= t
-    super
-    if buffer && @follow
-      follow_top = lines - buffer.content_height + 1
-      jump_to_line follow_top if topline < follow_top
+  def << s
+    unless buffer
+      BufferManager.spawn @buffer_name, self, :hidden => true, :system => true
     end
-  end
 
-  def << line
-    super
-    if buffer && @follow
+    s.split("\n").each { |l| super(l + "\n") } # insane. different << semantics.
+
+    if @follow
       follow_top = lines - buffer.content_height + 1
       jump_to_line follow_top if topline < follow_top
     end
@@ -39,6 +43,12 @@ class LogMode < TextMode
   def status
     super + " (follow: #@follow)"
   end
+
+  def cleanup
+    @on_kill.each { |cb| cb.call self }
+    self.text = ""
+    super
+  end
 end
 
 end
index 5849f3eed922e96a59519c2e99d972e8a0e98b4a..cf6134302a4992dca05557753ded1d5792df166c 100644 (file)
@@ -3,18 +3,16 @@ module Redwood
 class PollMode < LogMode
   def initialize
     @new = true
-    super
-  end
-
-  def puts s=""
-    self << s + "\n"
+    super "poll for new messages"
   end
 
   def poll
-    puts unless @new
-    @new = false
-    puts "Poll started at #{Time.now}"
-    PollManager.do_poll { |s| puts s }
+    unless @new
+      @new = false
+      self << "\n"
+    end
+    self << "Poll started at #{Time.now}\n"
+    PollManager.do_poll { |s| self << (s + "\n") }
   end
 end
 
index c79c5dbe51dd59df0426572c523a2b79492f524f..700dfc1f99d310a0290e0bfcab4e8478957ca678 100644 (file)
@@ -56,7 +56,7 @@ EOS
     ## don't check that it's an Account, though; assume they know what they're
     ## doing.
     if hook_reply_from && !(hook_reply_from.is_a? Person)
-      Redwood::log "reply-from returned non-Person, using default from."
+      info "reply-from returned non-Person, using default from."
       hook_reply_from = nil
     end
 
index 63b48ec00edd6e7b7cc50bd82b51d8b00c7c7ed9..c13142599e9aed1aaad9bde0c1a029b2331fa72e 100644 (file)
@@ -3,7 +3,7 @@ module Redwood
 class ScrollMode < Mode
   ## we define topline and botline as the top and bottom lines of any
   ## content in the currentview.
-  
+
   ## we left leftcol and rightcol as the left and right columns of any
   ## content in the current view. but since we're operating in a
   ## line-centric fashion, rightcol is always leftcol + the buffer
@@ -223,7 +223,7 @@ protected
       raise "nil text for color '#{color}'" if text.nil? # good for debugging
       l = text.display_length
       no_fill = i != a.size - 1
-      
+
       if xpos + l < @leftcol
         buffer.write ln - @topline, 0, "", :color => color,
                      :highlight => opts[:highlight]
index 273c02850f9cbc331d51dfb62a00ca05fbe9703d..7c9e7d87c0c1c6da3bf4e0b3634138ce6880aedd 100644 (file)
@@ -14,7 +14,7 @@ class TextMode < ScrollMode
     buffer.mark_dirty if buffer
     super()
   end
-  
+
   def save_to_disk
     fn = BufferManager.ask_for_filename :filename, "Save to file: ", @filename
     save_to_file(fn) { |f| f.puts text } if fn
@@ -50,7 +50,7 @@ class TextMode < ScrollMode
     @lines << @text.length
     if buffer
       ensure_mode_validity
-      buffer.mark_dirty 
+      buffer.mark_dirty
     end
   end
 
index b6711199e5269e6e6c67fb206c967ea44b63b044..fb6b2ce71c69b68011c622ef31b572f5288497d7 100644 (file)
@@ -477,7 +477,7 @@ EOS
       BufferManager.say("Saving threads...") do |say_id|
         dirty_threads.each_with_index do |t, i|
           BufferManager.say "Saving modified thread #{i + 1} of #{dirty_threads.length}...", say_id
-          t.save Index
+          t.save_state Index
         end
       end
     end
@@ -533,9 +533,9 @@ EOS
     keepl, modifyl = thread.labels.partition { |t| speciall.member? t }
 
     user_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", modifyl, @hidden_labels
-
     return unless user_labels
-    thread.labels = keepl + user_labels
+
+    thread.labels = Set.new(keepl) + user_labels
     user_labels.each { |l| LabelManager << l }
     update_text_for_line curpos
 
index 737f6f1ff309a5bd87ebdeb1d213c77c88b86bc2..dfe30ff7c4c49c4d163341aea1cdef87a85416ee 100644 (file)
@@ -212,14 +212,14 @@ EOS
     bt = to.size > 1 ? "#{to.size} recipients" : to.to_s
 
     if BufferManager.ask_yes_or_no "Really bounce to #{bt}?"
-      Redwood::log "Bounce Command: #{cmd}"
+      debug "bounce command: #{cmd}"
       begin
         IO.popen(cmd, 'w') do |sm|
           sm.puts m.raw_message
         end
         raise SendmailCommandFailed, "Couldn't execute #{cmd}" unless $? == 0
       rescue SystemCallError, SendmailCommandFailed => e
-        Redwood::log "Problem sending mail: #{e.message}"
+        warn "problem sending mail: #{e.message}"
         BufferManager.flash "Problem sending mail: #{e.message}"
       end
     end
@@ -253,7 +253,7 @@ EOS
     new_labels = BufferManager.ask_for_labels :label, "Labels for thread: ", @thread.labels
 
     return unless new_labels
-    @thread.labels = (reserved_labels + new_labels).uniq
+    @thread.labels = Set.new(reserved_labels) + new_labels
     new_labels.each { |l| LabelManager << l }
     update
     UpdateManager.relay self, :labeled, @thread.first
index 8a9d2188b7ec8f5f92e5bd44fc2beb94d475c282..46fe5c5ed2cc700c294e92a2a802be47fdd754cd 100644 (file)
@@ -35,22 +35,17 @@ EOS
     @thread = nil
     @last_poll = nil
     @polling = false
-    
-    self.class.i_am_the_instance self
-  end
-
-  def buffer
-    b, new = BufferManager.spawn_unless_exists("poll for new messages", :hidden => true, :system => true) { PollMode.new }
-    b
+    @mode = nil
   end
 
   def poll
     return if @polling
     @polling = true
+    @mode ||= PollMode.new
     HookManager.run "before-poll"
 
     BufferManager.flash "Polling for new messages..."
-    num, numi, from_and_subj, from_and_subj_inbox = buffer.mode.poll
+    num, numi, from_and_subj, from_and_subj_inbox = @mode.poll
     if num > 0
       BufferManager.flash "Loaded #{num.pluralize 'new message'}, #{numi} to inbox." 
     else
@@ -88,23 +83,36 @@ EOS
         begin
           yield "Loading from #{source}... " unless source.done? || (source.respond_to?(:has_errors?) && source.has_errors?)
         rescue SourceError => e
-          Redwood::log "problem getting messages from #{source}: #{e.message}"
+          warn "problem getting messages from #{source}: #{e.message}"
           Redwood::report_broken_sources :force_to_top => true
           next
         end
 
         num = 0
         numi = 0
-        add_messages_from source do |m_old, m, offset|
-          ## always preserve the labels on disk.
-          m.labels = ((m.labels - [:unread, :inbox]) + m_old.labels).uniq if m_old
-          yield "Found message at #{offset} with labels {#{m.labels * ', '}}"
-          unless m_old
+        each_message_from source do |m|
+          yield "Found message at #{m.source_info} with labels {#{m.labels.to_a * ', '}}"
+          old_m = Index.build_message m.id
+          if old_m
+              if old_m.source.id != source.id || old_m.source_info != m.source_info
+              ## here we merge labels between new and old versions, but we don't let the new
+              ## message add :unread or :inbox labels. (they can exist in the old version,
+              ## just not be added.)
+              new_labels = old_m.labels + (m.labels - [:unread, :inbox])
+              yield "Message at #{m.source_info} is an updated of an old message. Updating labels from #{m.labels.to_a * ','} => #{new_labels.to_a * ','}"
+              m.labels = new_labels
+              Index.update_message m
+            else
+              yield "Skipping already-imported message at #{m.source_info}"
+            end
+          else
+            yield "Found new message at #{m.source_info} with labels #{m.labels.to_a * ','}"
+            add_new_message m
             num += 1
             from_and_subj << [m.from && m.from.longname, m.subj]
-            if m.has_label?(:inbox) && ([:spam, :deleted, :killed] & m.labels).empty?
+            if (m.labels & [:inbox, :spam, :deleted, :killed]) == Set.new([:inbox])
               from_and_subj_inbox << [m.from && m.from.longname, m.subj]
-              numi += 1 
+              numi += 1
             end
           end
           m
@@ -121,47 +129,43 @@ EOS
     [total_num, total_numi, from_and_subj, from_and_subj_inbox]
   end
 
-  ## this is the main mechanism for adding new messages to the
-  ## index. it's called both by sup-sync and by PollMode.
-  ##
-  ## for each message in the source, starting from the source's
-  ## starting offset, this methods yields the message, the source
-  ## offset, and the index entry on disk (if any). it expects the
-  ## yield to return the message (possibly altered in some way), and
-  ## then adds it (if new) or updates it (if previously seen).
+  ## like Source#each, but yields successive Message objects, which have their
+  ## labels and offsets set correctly.
   ##
-  ## the labels of the yielded message are the default source
-  ## labels. it is likely that callers will want to replace these with
-  ## the index labels, if they exist, so that state is not lost when
-  ## e.g. a new version of a message from a mailing list comes in.
-  def add_messages_from source, opts={}
+  ## this is the primary mechanism for iterating over messages from a source.
+  def each_message_from source, opts={}
     begin
       return if source.done? || source.has_errors?
 
-      source.each do |offset, default_labels|
+      source.each do |offset, source_labels|
         if source.has_errors?
-          Redwood::log "error loading messages from #{source}: #{source.error.message}"
+          warn "error loading messages from #{source}: #{source.error.message}"
           return
         end
 
-        m_new = Message.build_from_source source, offset
-        m_old = Index.build_message m_new.id
+        m = Message.build_from_source source, offset
+        m.labels += source_labels + (source.archived? ? [] : [:inbox])
+        m.labels.delete :unread if m.source_marked_read? # preserve read status if possible
+        m.labels.each { |l| LabelManager << l }
 
-        m_new.labels += default_labels + (source.archived? ? [] : [:inbox])
-        m_new.labels << :sent if source.uri.eql?(SentManager.source_uri)
-        m_new.labels.delete :unread if m_new.source_marked_read?
-        m_new.labels.each { |l| LabelManager << l }
-
-        HookManager.run "before-add-message", :message => m_new
-        m_ret = yield(m_old, m_new, offset) or next if block_given?
-        Index.sync_message m_ret, opts
-        UpdateManager.relay self, :added, m_ret unless m_old
+        HookManager.run "before-add-message", :message => m
+        yield m
       end
     rescue SourceError => e
-      Redwood::log "problem getting messages from #{source}: #{e.message}"
+      warn "problem getting messages from #{source}: #{e.message}"
       Redwood::report_broken_sources :force_to_top => true
     end
   end
+
+  ## TODO: see if we can do this within PollMode rather than by calling this
+  ## method.
+  ##
+  ## a wrapper around Index.add_message that calls the proper hooks,
+  ## does the gui callback stuff, etc.
+  def add_new_message m
+    Index.add_message m
+    UpdateManager.relay self, :added, m
+  end
 end
 
 end
index b750d714de4d93df75f5001177e97858f4744adc..9203dd666816a770d8537c18c6281ae8f17ed14a 100644 (file)
@@ -8,8 +8,6 @@ class SentManager
   def initialize source_uri
     @source = nil
     @source_uri = source_uri
-    self.class.i_am_the_instance self
-    Redwood::log "SentManager intialized with source uri: #@source_uri"
   end
 
   def source_id; @source.id; end
@@ -22,7 +20,6 @@ class SentManager
 
   def default_source
     @source = Recoverable.new SentLoader.new
-    Redwood::log "SentManager initializing default source: #@source."
     @source_uri = @source.uri
     @source
   end
@@ -30,9 +27,9 @@ class SentManager
   def write_sent_message date, from_email, &block
     @source.store_message date, from_email, &block
 
-    PollManager.add_messages_from(@source) do |m_old, m, offset|
+    PollManager.each_message_from(@source) do |m|
       m.remove_label :unread
-      m
+      PollManager.add_new_message m
     end
   end
 end
@@ -52,7 +49,7 @@ class SentLoader < MBox::Loader
   def uri; 'sup://sent' end
 
   def id; 9998; end
-  def labels; [:inbox]; end
+  def labels; [:inbox, :sent]; end
 end
 
 end
index 1bb77973f0d609d6441c726ada6c7b2236a28d03..78386ff3fd19b0f74c75820827346df036f84c61 100644 (file)
@@ -34,12 +34,12 @@ class Source
   ## To write a new source, subclass this class, and implement:
   ##
   ## - start_offset
-  ## - end_offset (exclusive!)
+  ## - end_offset (exclusive!) (or, #done?)
   ## - load_header offset
   ## - load_message offset
   ## - raw_header offset
   ## - raw_message offset
-  ## - check
+  ## - check (optional)
   ## - next (or each, if you prefer): should return a message and an
   ##   array of labels.
   ##
@@ -78,6 +78,7 @@ class Source
     @dirty = false
   end
 
+  ## overwrite me if you have a disk incarnation (currently used only for sup-sync-back)
   def file_path; nil end
 
   def to_s; @uri.to_s; end
@@ -92,20 +93,23 @@ class Source
   ## to proactively notify the user of any source problems.
   def check; end
 
+  ## yields successive offsets and labels, starting at #cur_offset.
+  ##
+  ## when implementing a source, you can overwrite either #each or #next. the
+  ## default #each just calls next over and over.
   def each
     self.cur_offset ||= start_offset
     until done?
-      n, labels = self.next
-      raise "no message" unless n
-      yield n, labels
+      offset, labels = self.next
+      yield offset, labels
     end
   end
 
-  ## read a raw email header from a filehandle (or anything that responds to
-  ## #gets), and turn it into a hash of key-value pairs.
+  ## utility method to read a raw email header from an IO stream and turn it
+  ## into a hash of key-value pairs. minor special semantics for certain headers.
   ##
-  ## WARNING! THIS IS A SPEED-CRITICAL SECTION. Everything you do here will have
-  ## significant effect on Sup's processing speed of email from ALL sources.
+  ## THIS IS A SPEED-CRITICAL SECTION. Everything you do here will have a
+  ## significant effect on Sup's processing speed of email from ALL sources.
   ## Little things like string interpolation, regexp interpolation, += vs <<,
   ## all have DRAMATIC effects. BE CAREFUL WHAT YOU DO!
   def self.parse_raw_email_header f
@@ -116,9 +120,11 @@ class Source
       case line
       ## these three can occur multiple times, and we want the first one
       when /^(Delivered-To|X-Original-To|Envelope-To):\s*(.*?)\s*$/i; header[last = $1.downcase] ||= $2
-      ## mark this guy specially. not sure why i care.
+      ## regular header: overwrite (not that we should see more than one)
+      ## TODO: figure out whether just using the first occurrence changes
+      ## anything (which would simplify the logic slightly)
       when /^([^:\s]+):\s*(.*?)\s*$/i; header[last = $1.downcase] = $2
-      when /^\r*$/; break
+      when /^\r*$/; break # blank line signifies end of header
       else
         if last
           header[last] << " " unless header[last].empty?
@@ -133,7 +139,7 @@ class Source
       header[k] = begin
         Rfc2047.decode_to $encoding, v
       rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::IllegalSequence => e
-        #Redwood::log "warning: error decoding RFC 2047 header (#{e.class.name}): #{e.message}"
+        #debug "warning: error decoding RFC 2047 header (#{e.class.name}): #{e.message}"
         v
       end
     end
@@ -144,7 +150,7 @@ protected
 
   ## convenience function
   def parse_raw_email_header f; self.class.parse_raw_email_header f end
-  
+
   def Source.expand_filesystem_uri uri
     uri.gsub "~", File.expand_path("~")
   end
@@ -155,6 +161,21 @@ protected
   end
 end
 
+## if you have a @labels instance variable, include this
+## to serialize them nicely as an array, rather than as a
+## nasty set.
+module SerializeLabelsNicely
+  def before_marshal # can return an object
+    c = clone
+    c.instance_eval { @labels = @labels.to_a.map { |l| l.to_s } }
+    c
+  end
+
+  def after_unmarshal!
+    @labels = Set.new(@labels.map { |s| s.to_sym })
+  end
+end
+
 class SourceManager
   include Singleton
 
@@ -162,7 +183,6 @@ class SourceManager
     @sources = {}
     @sources_dirty = false
     @source_mutex = Monitor.new
-    self.class.i_am_the_instance self
   end
 
   def [](id)
@@ -204,7 +224,7 @@ class SourceManager
           File.chmod 0600, fn
           FileUtils.mv fn, bakfn, :force => true unless File.exists?(bakfn) && File.size(fn) == 0
         end
-        Redwood::save_yaml_obj sources.sort_by { |s| s.id.to_i }, fn, true
+        Redwood::save_yaml_obj sources, fn, true
         File.chmod 0600, fn
       end
       @sources_dirty = false
diff --git a/lib/sup/suicide.rb b/lib/sup/suicide.rb
deleted file mode 100644 (file)
index 98b4346..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-module Redwood
-
-class SuicideManager
-  include Singleton
-
-  DELAY = 5
-
-  def initialize fn
-    @fn = fn
-    @die = false
-    @thread = nil
-    self.class.i_am_the_instance self
-    FileUtils.rm_f @fn
-  end
-
-  bool_reader :die
-
-  def start
-    @thread = Redwood::reporting_thread("suicide watch") do
-      while true
-        sleep DELAY
-        if File.exists? @fn
-          FileUtils.rm_f @fn
-          @die = true
-        end
-      end
-    end
-  end
-
-  def stop
-    @thread.kill if @thread
-    @thread = nil
-  end
-end
-
-end
index c748c7a653a6a3405a089c2cfe4149a17ba82de4..76803bf97c9a00bc394a6f9e59c4f26b6b318b9e 100644 (file)
@@ -35,9 +35,9 @@ class TextField
     @completion_block = block
     @field = Ncurses::Form.new_field 1, @width - question.length, @y, @x + question.length, 256, 0
     @form = Ncurses::Form.new_form [@field]
-    @value = default
+    @value = default || ''
     Ncurses::Form.post_form @form
-    set_cursed_value default if default
+    set_cursed_value @value
   end
 
   def position_cursor
@@ -112,11 +112,11 @@ class TextField
         unless @history.empty?
           value = get_cursed_value
           @i ||= @history.size
-          #Redwood::log "history before #{@history.inspect}"
+          #debug "history before #{@history.inspect}"
           @history[@i] = value #unless value =~ /^\s*$/
           @i = (@i + (c == Ncurses::KEY_UP ? -1 : 1)) % @history.size
           @value = @history[@i]
-          #Redwood::log "history after #{@history.inspect}"
+          #debug "history after #{@history.inspect}"
           set_cursed_value @value
           Ncurses::Form::REQ_END_FIELD
         end
index d395c3588f7274448dc86d2d784ee9021b29c3b0..2300305c0710da7dfcb55891b5967abdc27e7f8f 100644 (file)
@@ -24,6 +24,8 @@
 ## a faked root object tying them all together into one tree
 ## structure.
 
+require 'set'
+
 module Redwood
 
 class Thread
@@ -101,17 +103,16 @@ class Thread
   def toggle_label label
     if has_label? label
       remove_label label
-      return false
+      false
     else
       apply_label label
-      return true
+      true
     end
   end
 
   def set_labels l; each { |m, *o| m && m.labels = l }; end
-  
   def has_label? t; any? { |m, *o| m && m.has_label?(t) }; end
-  def save index; each { |m, *o| m && m.save(index) }; end
+  def save_state index; each { |m, *o| m && m.save_state(index) }; end
 
   def direct_participants
     map { |m, *o| [m.from] + m.to if m }.flatten.compact.uniq
@@ -123,15 +124,14 @@ class Thread
 
   def size; map { |m, *o| m ? 1 : 0 }.sum; end
   def subj; argfind { |m, *o| m && m.subj }; end
-  def labels
-      map { |m, *o| m && m.labels }.flatten.compact.uniq.sort_by { |t| t.to_s }
-  end
+  def labels; inject(Set.new) { |s, (m, *o)| m ? s | m.labels : s } end
   def labels= l
-    each { |m, *o| m && m.labels = l.clone }
+    raise ArgumentError, "not a set" unless l.is_a?(Set)
+    each { |m, *o| m && m.labels = l.dup }
   end
 
   def latest_message
-    inject(nil) do |a, b| 
+    inject(nil) do |a, b|
       b = b.first
       if a.nil?
         b
@@ -162,7 +162,7 @@ class Container
     @id = id
     @message, @parent, @thread = nil, nil, nil
     @children = []
-  end      
+  end
 
   def each_with_stuff parent=nil
     yield self, 0, parent
@@ -357,7 +357,7 @@ class ThreadSet
     return if threads.size < 2
 
     containers = threads.map do |t|
-      c = @messages.member?(c) ? @messages[t.first.id] : nil
+      c = @messages.member?(t.first.id) ? @messages[t.first.id] : nil
       raise "not in threadset: #{t.first.id}" unless c && c.message
       c
     end
index 5a93c31bc1dac91142c9c4bb509cd7ee3649756d..9ccf84a01724af3d7f7a2077e9f774f85d9579a8 100644 (file)
@@ -12,7 +12,6 @@ class UndoManager
 
   def initialize
     @@actionlist = []
-    self.class.i_am_the_instance self
   end
 
   def register desc, *actions, &b
index 021b7e1b09eb93276bc03d659d36fb749f5c9aec..d3868012cd0b3607c17914729a170bf9ff4fbb47 100644 (file)
@@ -16,7 +16,6 @@ class UpdateManager
 
   def initialize
     @targets = {}
-    self.class.i_am_the_instance self
   end
 
   def register o; @targets[o] = true; end
index 3f2c901a160823c4229d922fd1c052fa42e6a428..068ce6bad904c9012bdcd5d8883ac48a68e59976 100644 (file)
@@ -2,6 +2,7 @@ require 'thread'
 require 'lockfile'
 require 'mime/types'
 require 'pathname'
+require 'set'
 
 ## time for some monkeypatching!
 class Lockfile
@@ -24,6 +25,7 @@ class Lockfile
   def lockinfo_on_disk
     h = load_lock_id IO.read(path)
     h['mtime'] = File.mtime path
+    h['path'] = path
     h
   end
 
@@ -90,7 +92,7 @@ end
 
 class Range
   ## only valid for integer ranges (unless I guess it's exclusive)
-  def size 
+  def size
     last - first + (exclude_end? ? 0 : 1)
   end
 end
@@ -215,8 +217,8 @@ class String
       newpos = case state
         when :escaped_instring, :escaped_outstring then pos
         else index(/[,"\\]/, pos)
-      end 
-      
+      end
+
       if newpos
         char = self[newpos]
       else
@@ -288,10 +290,12 @@ class String
     end
   end
 
-  ## takes a space-separated list of words, and returns an array of symbols.
-  ## typically used in Sup for translating Ferret's representation of a list
-  ## of labels (a string) to an array of label symbols.
-  def symbolistize; split.map { |x| x.intern } end
+  ## takes a list of words, and returns an array of symbols.  typically used in
+  ## Sup for translating Ferret's representation of a list of labels (a string)
+  ## to an array of label symbols.
+  ##
+  ## split_on will be passed to String#split, so you can leave this nil for space.
+  def to_set_of_symbols split_on=nil; Set.new split(split_on).map { |x| x.strip.intern } end
 end
 
 class Numeric
@@ -419,10 +423,6 @@ class Array
 
   def last= e; self[-1] = e end
   def nonempty?; !empty? end
-
-  def to_set_of_symbols
-    map { |x| x.is_a?(Symbol) ? x : x.intern }.uniq
-  end
 end
 
 class Time
@@ -496,19 +496,20 @@ class Time
   end
 end
 
-## simple singleton module. far less complete and insane than the ruby
-## standard library one, but automatically forwards methods calls and
-## allows for constructors that take arguments.
+## simple singleton module. far less complete and insane than the ruby standard
+## library one, but it automatically forwards methods calls and allows for
+## constructors that take arguments.
 ##
-## You must have #initialize call "self.class.i_am_the_instance self"
-## at some point or everything will fail horribly.
+## classes that inherit this can define initialize. however, you cannot call
+## .new on the class. To get the instance of the class, call .instance;
+## to create the instance, call init.
 module Singleton
   module ClassMethods
     def instance; @instance; end
     def instantiated?; defined?(@instance) && !@instance.nil?; end
     def deinstantiate!; @instance = nil; end
     def method_missing meth, *a, &b
-      raise "no instance defined!" unless defined? @instance
+      raise "no #{name} instance defined in method call to #{meth}!" unless defined? @instance
 
       ## if we've been deinstantiated, just drop all calls. this is
       ## useful because threads that might be active during the
@@ -518,13 +519,14 @@ module Singleton
 
       @instance.send meth, *a, &b
     end
-    def i_am_the_instance o
+    def init *args
       raise "there can be only one! (instance)" if defined? @instance
-      @instance = o
+      @instance = new(*args)
     end
   end
 
   def self.included klass
+    klass.private_class_method :allocate, :new
     klass.extend ClassMethods
   end
 end
@@ -543,7 +545,7 @@ class Recoverable
   def has_errors?; !@error.nil?; end
 
   def method_missing m, *a, &b; __pass m, *a, &b end
-  
+
   def id; __pass :id; end
   def to_s; __pass :to_s; end
   def to_yaml x; __pass :to_yaml, x; end
@@ -652,7 +654,7 @@ class Iconv
     begin
       Iconv.iconv(target + "//IGNORE", charset, text + " ").join[0 .. -2]
     rescue Errno::EINVAL, Iconv::InvalidEncoding, Iconv::InvalidCharacter, Iconv::IllegalSequence => e
-      Redwood::log "warning: error (#{e.class.name}) decoding text from #{charset} to #{target}: #{text[0 ... 20]}"
+      warn "couldn't transcode text from #{charset} to #{target} (\"#{text[0 ... 20]}\"...) (got #{e.message}); using original as is"
       text
     end
   end
index 861c2a3a7fc1618343da4f8ac51d62e36bd4993f..dbf66431bc263ada5420b9a981f766c9ca5675d4 100644 (file)
@@ -87,6 +87,10 @@ class XapianIndex < BaseIndex
       m
   end
 
+  def add_message m; sync_message m end
+  def update_message m; sync_message m end
+  def update_message_state m; sync_message m end
+
   def sync_message m, opts={}
     entry = synchronize { @entries[m.id] }
     snippet = m.snippet
@@ -100,7 +104,7 @@ class XapianIndex < BaseIndex
       :source_info => m.source_info,
       :date => (entry[:date] || m.date),
       :snippet => snippet,
-      :labels => labels.uniq,
+      :labels => labels,
       :from => (entry[:from] || [m.from.email, m.from.name]),
       :to => (entry[:to] || m.to.map { |p| [p.email, p.name] }),
       :cc => (entry[:cc] || m.cc.map { |p| [p.email, p.name] }),
@@ -110,7 +114,7 @@ class XapianIndex < BaseIndex
       :replytos => (entry[:replytos] || m.replytos),
     }
 
-    m.labels.each { |l| LabelManager << l }
+    labels.each { |l| LabelManager << l }
 
     synchronize do
       index_message m, opts
@@ -119,6 +123,7 @@ class XapianIndex < BaseIndex
     end
     true
   end
+  private :sync_message
 
   def num_results_for query={}
     xapian_query = build_xapian_query query
@@ -216,10 +221,10 @@ class XapianIndex < BaseIndex
       field, name = $1, ($3 || $4)
       case field
       when "filename"
-        Redwood::log "filename - translated #{field}:#{name} to attachment:\"#{name.downcase}\""
+        debug "filename: translated #{field}:#{name} to attachment:\"#{name.downcase}\""
         "attachment:\"#{name.downcase}\""
       when "filetype"
-        Redwood::log "filetype - translated #{field}:#{name} to attachment_extension:#{name.downcase}"
+        debug "filetype: translated #{field}:#{name} to attachment_extension:#{name.downcase}"
         "attachment_extension:#{name.downcase}"
       end
     end
@@ -233,13 +238,13 @@ class XapianIndex < BaseIndex
         if realdate
           case field
           when "after"
-            Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate.end}"
+            debug "chronic: translated #{field}:#{datestr} to #{realdate.end}"
             "date:#{realdate.end.to_i}..#{lastdate}"
           when "before"
-            Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate.begin}"
+            debug "chronic: translated #{field}:#{datestr} to #{realdate.begin}"
             "date:#{firstdate}..#{realdate.end.to_i}"
           else
-            Redwood::log "chronic: translated #{field}:#{datestr} to #{realdate}"
+            debug "chronic: translated #{field}:#{datestr} to #{realdate}"
             "date:#{realdate.begin.to_i}..#{realdate.end.to_i}"
           end
         else
@@ -408,10 +413,10 @@ class XapianIndex < BaseIndex
     m.attachments.each { |a| text << [a, PREFIX['attachment']] }
 
     truncated_date = if m.date < MIN_DATE
-      Redwood::log "warning: adjusting too-low date #{m.date} for indexing"
+      debug "warning: adjusting too-low date #{m.date} for indexing"
       MIN_DATE
     elsif m.date > MAX_DATE
-      Redwood::log "warning: adjusting too-high date #{m.date} for indexing"
+      debug "warning: adjusting too-high date #{m.date} for indexing"
       MAX_DATE
     else
       m.date