sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/
commit d8c74d2d33613f949fd6c4c1d91b03a8aeb4ff01
parent 9aa6b930c00b583641439834551dcae3491cebc4
Author: Gaute Hope <eg@gaute.vetsj.com>
Date:   Fri, 11 Apr 2014 09:16:10 +0200

Merge branch 'develop': For 0.17.0

Diffstat:
M CONTRIBUTORS | 33 ++++++++++++++++++---------------
M History.txt | 7 +++++++
M ReleaseNotes | 5 +++++
M bin/sup | 4 ++--
M bin/sup-tweak-labels | 2 +-
M devel/console.sh | 2 +-
M lib/sup.rb | 5 +++--
M lib/sup/account.rb | 2 +-
M lib/sup/buffer.rb | 4 ++--
M lib/sup/contact.rb | 2 +-
M lib/sup/crypto.rb | 2 +-
M lib/sup/draft.rb | 2 +-
M lib/sup/hook.rb | 2 +-
M lib/sup/idle.rb | 2 +-
M lib/sup/index.rb | 14 +++++++++-----
M lib/sup/label.rb | 2 +-
M lib/sup/logger.rb | 2 +-
M lib/sup/message.rb | 2 ++
M lib/sup/message_chunks.rb | 2 ++
M lib/sup/modes/compose_mode.rb | 4 ++--
M lib/sup/modes/contact_list_mode.rb | 2 +-
M lib/sup/modes/edit_message_async_mode.rb | 3 ++-
M lib/sup/modes/edit_message_mode.rb | 24 ++++++++++++++++++++----
M lib/sup/modes/forward_mode.rb | 2 +-
M lib/sup/modes/line_cursor_mode.rb | 25 ++++++++++++++++++++++---
M lib/sup/modes/thread_view_mode.rb | 20 +++++++++++---------
M lib/sup/person.rb | 2 +-
M lib/sup/poll.rb | 2 +-
M lib/sup/search.rb | 2 +-
M lib/sup/sent.rb | 2 +-
M lib/sup/source.rb | 2 +-
M lib/sup/thread.rb | 2 ++
M lib/sup/undo.rb | 2 +-
M lib/sup/update.rb | 2 +-
M lib/sup/util.rb | 62 ++++++++++++++++++++++++++++++++------------------------------
M lib/sup/util/ncurses.rb | 2 +-
M sup.gemspec | 2 +-
37 files changed, 162 insertions(+), 96 deletions(-)
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
@@ -14,13 +14,14 @@ Eric Sherman <hyperbolist at the gmail dot coms>
 Tero Tilus <tero at the tilus dot nets>
 Ben Walton <bwalton at the artsci.utoronto dot cas>
 Mike Stipicevic <stipim at the rpi dot edus>
+Martin Bähr <mbaehr at the societyserver dot orgs>
 Clint Byrum <clint at the ubuntu dot coms>
 Marcus Williams <marcus-sup at the bar-coded dot nets>
 Lionel Ott <white.magic at the gmx dot des>
 Gaudenz Steinlin <gaudenz at the soziologie dot chs>
 Matthieu Rakotojaona <matthieu.rakotojaona at the gmail dot coms>
-Ingmar Vanhassel <ingmar at the exherbo dot orgs>
 Mark Alexander <marka at the pobox dot coms>
+Ingmar Vanhassel <ingmar at the exherbo dot orgs>
 Edward Z. Yang <ezyang at the mit dot edus>
 Timon Vonk <timonv at the gmail dot coms>
 julien@macbook <julien.stechele at the gmail dot coms>
@@ -30,49 +31,51 @@ Richard Brown <rbrown at the exherbo dot orgs>
 Anthony Martinez <pi+sup at the pihost dot uss>
 Marc Hartstein <marc.hartstein at the alum.vassar dot edus>
 Israel Herraiz <israel.herraiz at the gmail dot coms>
+Christopher Corley <cscorley at the ua dot edus>
+Markus Klinik <mkl at the lambdanaut dot nets>
 Bo Borgerson <gigabo at the gmail dot coms>
 Atte Kojo <atte.kojo at the reaktor dot fis>
 Michael Hamann <michael at the content-space dot des>
-William Erik Baxter <web at the superscript dot coms>
 Jonathan Lassoff <jof at the thejof dot coms>
-Markus Klinik <markus.klinik at the gmx dot des>
+William Erik Baxter <web at the superscript dot coms>
 Grant Hollingworth <grant at the antiflux dot orgs>
-Ico Doornekamp <ico at the pruts dot nls>
 Adeodato Simó <dato at the net.com.org dot ess>
+Ico Doornekamp <ico at the pruts dot nls>
 Daniel Schoepe <daniel.schoepe at the googlemail dot coms>
 Jason Petsod <jason at the petsod dot orgs>
 James Taylor <james at the jamestaylor dot orgs>
-Steve Goldman <sgoldman at the tower-research dot coms>
 Robin Burchell <viroteck at the viroteck dot nets>
+Steve Goldman <sgoldman at the tower-research dot coms>
 Peter Harkins <ph at the malaprop dot orgs>
 Decklin Foster <decklin at the red-bean dot coms>
 Cameron Matheson <cam+sup at the cammunism dot orgs>
-Carl Worth <cworth at the cworth dot orgs>
 Alex Vandiver <alex at the chmrr dot nets>
-Jeff Balogh <its.jeff.balogh at the gmail dot coms>
+Carl Worth <cworth at the cworth dot orgs>
 Andrew Pimlott <andrew at the pimlott dot nets>
+Jeff Balogh <its.jeff.balogh at the gmail dot coms>
 Matías Aguirre <matiasaguirre at the gmail dot coms>
 Kornilios Kourtis <kkourt at the cslab.ece.ntua dot grs>
 Lars Fischer <fischer at the wiwi.uni-siegen dot des>
 Giorgio Lando <patroclo7 at the gmail dot coms>
 Kevin Riggle <kevinr at the free-dissociation dot coms>
 Benoît PIERRE <benoit.pierre at the gmail dot coms>
-Alvaro Herrera <alvherre at the alvh.no-ip dot orgs>
 Steven Lawrance <stl at the koffein dot nets>
+Alvaro Herrera <alvherre at the alvh.no-ip dot orgs>
 Jonah <Jonah at the GoodCoffee dot cas>
 ian <itaylor at the uark dot edus>
-MichaelRevell <mikearevell at the gmail dot coms>
-Per Andersson <avtobiff at the gmail dot coms>
-Todd Eisenberger <teisenbe at the andrew.cmu dot edus>
 Gregor Hoffleit <gregor at the sam.mediasupervision dot des>
+Per Andersson <avtobiff at the gmail dot coms>
 Adam Lloyd <adam at the alloy-d dot nets>
+Todd Eisenberger <teisenbe at the andrew.cmu dot edus>
+MichaelRevell <mikearevell at the gmail dot coms>
 0xACE <0xACE at the users.noreply.github dot coms>
 Steven Walter <swalter at the monarch.(none)>
-Jon M. Dugan <jdugan at the es dot nets>
+Steven Schmeiser <steven at the schmeiser dot orgs>
+Stefan Lundström <lundst at the snabb.(none)>
+William A. Kennington III <william at the wkennington dot coms>
+akojo <atte.kojo at the gmail dot coms>
 Horacio Sanson <horacio at the skillupjapan.co dot jps>
 Matthias Vallentin <vallentin at the icir dot orgs>
-akojo <atte.kojo at the gmail dot coms>
-William A. Kennington III <william at the wkennington dot coms>
-Stefan Lundström <lundst at the snabb.(none)>
+Jon M. Dugan <jdugan at the es dot nets>
 Johannes Larsen <johs.a.larsen at the gmail dot coms>
 Kirill Smelkov <kirr at the landau.phys.spbu dot rus>
diff --git a/History.txt b/History.txt
@@ -1,3 +1,10 @@
+== 0.17.0 / 2014-04-11
+
+* add continuous scrolling to thread view
+* add option for always editing in async mode
+* bugfix: fix completion char
+* bugfix: thread-view: dont close message when it is the first or last
+
 == 0.16.0 / 2014-03-21
 
 * sup-sync-back-mbox removed.
diff --git a/ReleaseNotes b/ReleaseNotes
@@ -1,3 +1,8 @@
+Release 0.17.0:
+
+Bugfixes and new option for continous scrolling as well as an option for
+always editing messages in async mode.
+
 Release 0.16.0:
 
 Removed unfinished and abandoned sup-sync-back-mbox.
diff --git a/bin/sup b/bin/sup
@@ -223,7 +223,7 @@ begin
     to = Person.from_address_list $opts[:compose]
     mode = ComposeMode.new :to => to, :subj => $opts[:subject]
     BufferManager.spawn "New Message", mode
-    mode.edit_message
+    mode.default_edit_message
   end
 
   unless $opts[:no_threads]
@@ -337,7 +337,7 @@ begin
         Index.each_id_by_date(:label => :draft) { |mid, builder| m = builder.call }
         r = ResumeMode.new(m)
         BufferManager.spawn "Edit message", r
-        r.edit_message
+        r.default_edit_message
       else
         b, new = BufferManager.spawn_unless_exists("All drafts") { LabelSearchResultsMode.new [:draft] }
         b.mode.load_threads :num => b.content_height if new
diff --git a/bin/sup-tweak-labels b/bin/sup-tweak-labels
@@ -85,7 +85,7 @@ begin
 
   parsed_query = index.parse_query query
   parsed_query.merge! :load_spam => true, :load_deleted => true, :load_killed => true
-  ids = Enumerator.new(index, :each_id, parsed_query)
+  ids = index.to_enum(:each_id, parsed_query)
   num_total = index.num_results_for parsed_query
 
   $stderr.puts "Found #{num_total} documents across #{source_ids.length} sources. Scanning..."
diff --git a/devel/console.sh b/devel/console.sh
@@ -1,3 +1,3 @@
 #!/bin/sh
 
-irb -I lib -r devel/start-console.rb
+irb -I lib -r ./devel/start-console.rb
diff --git a/lib/sup.rb b/lib/sup.rb
@@ -2,7 +2,6 @@
 
 require 'rubygems'
 require 'yaml'
-YAML::ENGINE.yamler = 'psych'
 require 'zlib'
 require 'thread'
 require 'fileutils'
@@ -333,7 +332,9 @@ EOM
       :slip_rows => 0,
       :col_jump => 2,
       :stem_language => "english",
-      :sync_back_to_maildir => false
+      :sync_back_to_maildir => false,
+      :continuous_scroll => false,
+      :always_edit_async => false,
     }
     if File.exists? filename
       config = Redwood::load_yaml_obj filename
diff --git a/lib/sup/account.rb b/lib/sup/account.rb
@@ -25,7 +25,7 @@ class Account < Person
 end
 
 class AccountManager
-  include Singleton
+  include Redwood::Singleton
 
   attr_accessor :default_account
 
diff --git a/lib/sup/buffer.rb b/lib/sup/buffer.rb
@@ -99,7 +99,7 @@ class Buffer
 end
 
 class BufferManager
-  include Singleton
+  include Redwood::Singleton
 
   attr_reader :focus_buf
 
@@ -547,7 +547,7 @@ EOS
         kill_buffer completion_buf if completion_buf
 
         shorts = tf.completions.map { |full, short| short }
-        prefix_len = shorts.shared_prefix.length
+        prefix_len = shorts.shared_prefix(caseless=true).length
 
         mode = CompletionMode.new shorts, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len
         completion_buf = spawn "<completions>", mode, :height => 10
diff --git a/lib/sup/contact.rb b/lib/sup/contact.rb
@@ -3,7 +3,7 @@
 module Redwood
 
 class ContactManager
-  include Singleton
+  include Redwood::Singleton
 
   def initialize fn
     @fn = fn
diff --git a/lib/sup/crypto.rb b/lib/sup/crypto.rb
@@ -6,7 +6,7 @@ end
 module Redwood
 
 class CryptoManager
-  include Singleton
+  include Redwood::Singleton
 
   class Error < StandardError; end
 
diff --git a/lib/sup/draft.rb b/lib/sup/draft.rb
@@ -1,7 +1,7 @@
 module Redwood
 
 class DraftManager
-  include Singleton
+  include Redwood::Singleton
 
   attr_accessor :source
   def initialize dir
diff --git a/lib/sup/hook.rb b/lib/sup/hook.rb
@@ -69,7 +69,7 @@ class HookManager
     end
   end
 
-  include Singleton
+  include Redwood::Singleton
 
   @descs = {}
 
diff --git a/lib/sup/idle.rb b/lib/sup/idle.rb
@@ -3,7 +3,7 @@ require 'thread'
 module Redwood
 
 class IdleManager
-  include Singleton
+  include Redwood::Singleton
 
   IDLE_THRESHOLD = 60
 
diff --git a/lib/sup/index.rb b/lib/sup/index.rb
@@ -55,7 +55,7 @@ EOS
     def method_missing m; @h[m.to_s] end
   end
 
-  include Singleton
+  include Redwood::Singleton
 
   def initialize dir=BASE_DIR
     @dir = dir
@@ -98,9 +98,9 @@ EOS
     end
   end
 
-  def load
+  def load failsafe=false
     SourceManager.load_sources
-    load_index
+    load_index failsafe
   end
 
   def save
@@ -110,7 +110,11 @@ EOS
     save_index
   end
 
-  def load_index
+  def get_xapian
+    @xapian
+  end
+
+  def load_index failsafe=false
     path = File.join(@dir, 'xapian')
     if File.exists? path
       @xapian = Xapian::WritableDatabase.new(path, Xapian::DB_OPEN)
@@ -329,7 +333,7 @@ EOS
     synchronize { get_entry(id)[:source_id] }
   end
 
-  ## Yields each tearm in the index that starts with prefix
+  ## Yields each term in the index that starts with prefix
   def each_prefixed_term prefix
     term = @xapian._dangerous_allterms_begin prefix
     lastTerm = @xapian._dangerous_allterms_end prefix
diff --git a/lib/sup/label.rb b/lib/sup/label.rb
@@ -3,7 +3,7 @@
 module Redwood
 
 class LabelManager
-  include Singleton
+  include Redwood::Singleton
 
   ## labels that have special semantics. user will be unable to
   ## add/remove these via normal label mechanisms.
diff --git a/lib/sup/logger.rb b/lib/sup/logger.rb
@@ -8,7 +8,7 @@ module Redwood
 ## also keeps a record of all messages, so that adding a new sink will send all
 ## previous messages to it by default.
 class Logger
-  include Singleton
+  include Redwood::Singleton
 
   LEVELS = %w(debug info warn error) # in order!
 
diff --git a/lib/sup/message.rb b/lib/sup/message.rb
@@ -1,3 +1,5 @@
+# encoding: UTF-8
+
 require 'time'
 
 module Redwood
diff --git a/lib/sup/message_chunks.rb b/lib/sup/message_chunks.rb
@@ -1,3 +1,5 @@
+# encoding: UTF-8
+
 require 'tempfile'
 require 'rbconfig'
 require 'shellwords'
diff --git a/lib/sup/modes/compose_mode.rb b/lib/sup/modes/compose_mode.rb
@@ -16,7 +16,7 @@ class ComposeMode < EditMessageMode
     super :header => header, :body => (opts[:body] || [])
   end
 
-  def edit_message
+  def default_edit_message
     edited = super
     BufferManager.kill_buffer self.buffer unless edited
     edited
@@ -31,7 +31,7 @@ class ComposeMode < EditMessageMode
 
     mode = ComposeMode.new :from => from, :to => to, :cc => cc, :bcc => bcc, :subj => subj
     BufferManager.spawn "New Message", mode
-    mode.edit_message
+    mode.default_edit_message
   end
 end
 
diff --git a/lib/sup/modes/contact_list_mode.rb b/lib/sup/modes/contact_list_mode.rb
@@ -71,7 +71,7 @@ class ContactListMode < LineCursorMode
     when :regular
       mode = ComposeMode.new :to => people
       BufferManager.spawn "new message", mode
-      mode.edit_message
+      mode.default_edit_message
     end
   end
 
diff --git a/lib/sup/modes/edit_message_async_mode.rb b/lib/sup/modes/edit_message_async_mode.rb
@@ -29,11 +29,12 @@ EOS
     @orig_mtime = File.mtime @file_path
 
     @text = ["ASYNC MESSAGE EDIT",
-             "", "Your message with subject:",  msg_subject, "is saved in a file:", "", @file_path, "", 
+             "", "Your message with subject:",  msg_subject, "is saved in a file:", "", @file_path, "",
              "You can edit your message in the editor of your choice and continue to",
              "use sup while you edit your message.", "",
              "Press <Enter> to have the file path copied to the clipboard.", "",
              "When you have finished editing, select this buffer and press 'E'.",]
+    run_async_hook()
     super()
   end
 
diff --git a/lib/sup/modes/edit_message_mode.rb b/lib/sup/modes/edit_message_mode.rb
@@ -79,8 +79,8 @@ EOS
     k.add :edit_to, "Edit To:", 't'
     k.add :edit_cc, "Edit Cc:", 'c'
     k.add :edit_subject, "Edit Subject", 's'
-    k.add :edit_message, "Edit message", :enter
-    k.add :edit_message_async, "Edit message asynchronously", 'E'
+    k.add :default_edit_message, "Edit message (default)", :enter
+    k.add :alternate_edit_message, "Edit message (alternate, asynchronously)", 'E'
     k.add :save_as_draft, "Save as draft", 'P'
     k.add :attach_file, "Attach a file", 'a'
     k.add :delete_attachment, "Delete an attachment", 'd'
@@ -105,7 +105,7 @@ EOS
     begin
       hostname = File.open("/etc/mailname", "r").gets.chomp
     rescue
-        nil
+      nil
     end
     hostname = Socket.gethostname if hostname.nil? or hostname.empty?
 
@@ -182,7 +182,7 @@ EOS
     if lines > curpos
       return
     elsif (curpos - lines) >= @header_lines.length
-      edit_message
+      default_edit_message
     else
       edit_field @header_lines[curpos - lines]
     end
@@ -226,6 +226,22 @@ EOS
     end
   end
 
+  def default_edit_message
+    if $config[:always_edit_async]
+      return edit_message_async
+    else
+      return edit_message
+    end
+  end
+
+  def alternate_edit_message
+    if $config[:always_edit_async]
+      return edit_message
+    else
+      return edit_message_async
+    end
+  end
+
   def edit_message
     old_from = @header["From"] if @account_selector
 
diff --git a/lib/sup/modes/forward_mode.rb b/lib/sup/modes/forward_mode.rb
@@ -59,7 +59,7 @@ class ForwardMode < EditMessageMode
       end
 
     BufferManager.spawn title, mode
-    mode.edit_message
+    mode.default_edit_message
   end
 
 protected
diff --git a/lib/sup/modes/line_cursor_mode.rb b/lib/sup/modes/line_cursor_mode.rb
@@ -94,7 +94,17 @@ protected
     call_load_more_callbacks buffer.content_height if @curpos >= lines - [buffer.content_height/2,1].max
     return false unless @curpos < lines - 1
 
-    if @curpos >= botline - 1
+    if $config[:continuous_scroll] and (@curpos == botline - 3 and @curpos < lines - 3)
+      # load more lines, one at a time.
+      jump_to_line topline + 1
+      @curpos += 1
+      unless buffer.dirty?
+        draw_line @curpos - 1
+        draw_line @curpos
+        set_status
+        buffer.commit
+      end
+    elsif @curpos >= botline - 1
       page_down
       set_cursor_pos topline
     else
@@ -111,7 +121,17 @@ protected
 
   def cursor_up
     return false unless @curpos > @cursor_top
-    if @curpos == topline
+
+    if $config[:continuous_scroll] and (@curpos == topline + 2)
+      jump_to_line topline - 1
+      @curpos -= 1
+      unless buffer.dirty?
+        draw_line @curpos + 1
+        draw_line @curpos
+        set_status
+        buffer.commit
+      end
+    elsif @curpos == topline
       old_topline = topline
       page_up
       set_cursor_pos [old_topline - 1, topline].max
@@ -180,5 +200,4 @@ private
     @load_more_q.push size if $config[:load_more_threads_when_scrolling]
   end
 end
-
 end
diff --git a/lib/sup/modes/thread_view_mode.rb b/lib/sup/modes/thread_view_mode.rb
@@ -1,5 +1,3 @@
-require 'shellwords'
-
 module Redwood
 
 class ThreadViewMode < LineCursorMode
@@ -354,7 +352,7 @@ EOS
     m = @message_lines[curpos] or return
     mode = ComposeMode.new(:body => m.quotable_body_lines, :to => m.to, :cc => m.cc, :subj => m.subj, :bcc => m.bcc, :refs => m.refs, :replytos => m.replytos)
     BufferManager.spawn "edit as new", mode
-    mode.edit_message
+    mode.default_edit_message
   end
 
   def save_to_disk
@@ -363,12 +361,12 @@ EOS
     when Chunk::Attachment
       default_dir = $config[:default_attachment_save_dir]
       default_dir = ENV["HOME"] if default_dir.nil? || default_dir.empty?
-      default_fn = File.expand_path File.join(default_dir, Shellwords.escape(chunk.filename))
+      default_fn = File.expand_path File.join(default_dir, chunk.filename)
       fn = BufferManager.ask_for_filename :filename, "Save attachment to file or directory: ", default_fn, true
 
       # if user selects directory use file name from message
       if fn and File.directory? fn
-        fn = File.join(fn, Shellwords.escape(chunk.filename))
+        fn = File.join(fn, chunk.filename)
       end
 
       save_to_file(fn) { |f| f.print chunk.raw_content } if fn
@@ -392,7 +390,7 @@ EOS
     num_errors = 0
     m.chunks.each do |chunk|
       next unless chunk.is_a?(Chunk::Attachment)
-      fn = File.join(folder, Shellwords.escape(chunk.filename))
+      fn = File.join(folder, chunk.filename)
       num_errors += 1 unless save_to_file(fn, false) { |f| f.print chunk.raw_content }
       num += 1
     end
@@ -423,7 +421,7 @@ EOS
       mode = ResumeMode.new m
       BufferManager.spawn "Edit message", mode
       BufferManager.kill_buffer self.buffer
-      mode.edit_message
+      mode.default_edit_message
     else
       BufferManager.flash "Not a draft message!"
     end
@@ -456,13 +454,15 @@ EOS
     m = (curpos ... @message_lines.length).argfind { |i| @message_lines[i] }
     return unless m
 
+    nextm = @layout[m].next
+    return unless nextm
+
     if @layout[m].toggled_state == true
       @layout[m].state = :closed
       @layout[m].toggled_state = false
       update
     end
 
-    nextm = @layout[m].next
     if @layout[nextm].state == :closed
       @layout[nextm].state = :open
       @layout[nextm].toggled_state = true
@@ -493,13 +493,15 @@ EOS
     m = (0 .. curpos).to_a.reverse.argfind { |i| @message_lines[i] }
     return unless m
 
+    nextm = @layout[m].prev
+    return unless nextm
+
     if @layout[m].toggled_state == true
       @layout[m].state = :closed
       @layout[m].toggled_state = false
       update
     end
 
-    nextm = @layout[m].prev
     if @layout[nextm].state == :closed
       @layout[nextm].state = :open
       @layout[nextm].toggled_state = true
diff --git a/lib/sup/person.rb b/lib/sup/person.rb
@@ -119,7 +119,7 @@ class Person
 
   def self.from_address_list ss
     return [] if ss.nil?
-    ss.split_on_commas.map { |s| self.from_address s }
+    ss.dup.split_on_commas.map { |s| self.from_address s }
   end
 
   ## see comments in self.from_address
diff --git a/lib/sup/poll.rb b/lib/sup/poll.rb
@@ -3,7 +3,7 @@ require 'thread'
 module Redwood
 
 class PollManager
-  include Singleton
+  include Redwood::Singleton
 
   HookManager.register "before-add-message", <<EOS
 Executes immediately before a message is added to the index.
diff --git a/lib/sup/search.rb b/lib/sup/search.rb
@@ -3,7 +3,7 @@
 module Redwood
 
 class SearchManager
-  include Singleton
+  include Redwood::Singleton
 
   class ExpansionError < StandardError; end
 
diff --git a/lib/sup/sent.rb b/lib/sup/sent.rb
@@ -1,7 +1,7 @@
 module Redwood
 
 class SentManager
-  include Singleton
+  include Redwood::Singleton
 
   attr_reader :source, :source_uri
 
diff --git a/lib/sup/source.rb b/lib/sup/source.rb
@@ -187,7 +187,7 @@ module SerializeLabelsNicely
 end
 
 class SourceManager
-  include Singleton
+  include Redwood::Singleton
 
   def initialize
     @sources = {}
diff --git a/lib/sup/thread.rb b/lib/sup/thread.rb
@@ -1,3 +1,5 @@
+# encoding: UTF-8
+#
 ## Herein lies all the code responsible for threading messages. It's
 ## basically an online version of the JWZ threading algorithm:
 ## http://www.jwz.org/doc/threading.html
diff --git a/lib/sup/undo.rb b/lib/sup/undo.rb
@@ -8,7 +8,7 @@ module Redwood
 ## undo the archival action
 
 class UndoManager
-  include Singleton
+  include Redwood::Singleton
 
   def initialize
     @@actionlist = []
diff --git a/lib/sup/update.rb b/lib/sup/update.rb
@@ -12,7 +12,7 @@ module Redwood
 ## single "view". Luckily, that's true.)
 
 class UpdateManager
-  include Singleton
+  include Redwood::Singleton
 
   def initialize
     @targets = {}
diff --git a/lib/sup/util.rb b/lib/sup/util.rb
@@ -632,39 +632,41 @@ end
 ## 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 #{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
-      ## cleanup process (e.g. polling) would otherwise have to
-      ## special-case every call to a Singleton object
-      return nil if @instance.nil?
-
-      # Speed up further calls by defining a shortcut around method_missing
-      if meth.to_s[-1,1] == '='
-        # Argh! Inconsistency! Setters do not work like all the other methods.
-        class_eval "def self.#{meth}(a); @instance.send :#{meth}, a; end"
-      else
-        class_eval "def self.#{meth}(*a, &b); @instance.send :#{meth}, *a, &b; end"
-      end
+module Redwood
+  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 #{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
+        ## cleanup process (e.g. polling) would otherwise have to
+        ## special-case every call to a Singleton object
+        return nil if @instance.nil?
+
+        # Speed up further calls by defining a shortcut around method_missing
+        if meth.to_s[-1,1] == '='
+          # Argh! Inconsistency! Setters do not work like all the other methods.
+          class_eval "def self.#{meth}(a); @instance.send :#{meth}, a; end"
+        else
+          class_eval "def self.#{meth}(*a, &b); @instance.send :#{meth}, *a, &b; end"
+        end
 
-      @instance.send meth, *a, &b
-    end
-    def init *args
-      raise "there can be only one! (instance)" if instantiated?
-      @instance = new(*args)
+        @instance.send meth, *a, &b
+      end
+      def init *args
+        raise "there can be only one! (instance)" if instantiated?
+        @instance = new(*args)
+      end
     end
-  end
 
-  def self.included klass
-    klass.private_class_method :allocate, :new
-    klass.extend ClassMethods
+    def self.included klass
+      klass.private_class_method :allocate, :new
+      klass.extend ClassMethods
+    end
   end
 end
 
diff --git a/lib/sup/util/ncurses.rb b/lib/sup/util/ncurses.rb
@@ -116,7 +116,7 @@ module Ncurses
     # Empty singleton that
     # keeps GC from going crazy.
     class Empty < CharCode
-      include Singleton
+      include Redwood::Singleton
 
       ## Wrap methods that may change us
       ## and generate new object instead.
diff --git a/sup.gemspec b/sup.gemspec
@@ -45,7 +45,7 @@ SUP: If you are upgrading Sup from before version 0.14.0: Please
     s.files = SUP_FILES
     s.executables = SUP_EXECUTABLES
 
-    s.required_ruby_version = '>= 1.9.2'
+    s.required_ruby_version = '>= 1.9.3'
 
     s.add_runtime_dependency "xapian-ruby", "~> 1.2.15"
     s.add_runtime_dependency "ncursesw", "~> 1.4.0"