sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/
commit 2475a1732dac0db1889a40cf0d15787a12d65dfc
parent 5cc075a85334b2ded420286c7139538599578f9d
Author: Gaute Hope <eg@gaute.vetsj.com>
Date:   Fri, 20 Dec 2013 10:25:28 +0100

Merge branch 'develop'

Diffstat:
M CONTRIBUTORS | 24 +++++++++++++-----------
M History.txt | 7 +++++++
M ReleaseNotes | 4 ++++
M bin/sup | 12 +++++++-----
M lib/sup.rb | 1 +
M lib/sup/account.rb | 52 ++++++++++++++++++++++++++++++++++++++++++----------
M lib/sup/buffer.rb | 92 ++++++++++++++++++-------------------------------------------------------------
M lib/sup/index.rb | 16 ++++++++--------
M lib/sup/keymap.rb | 6 ++++--
M lib/sup/message_chunks.rb | 1 +
M lib/sup/modes/thread_index_mode.rb | 2 +-
M lib/sup/tagger.rb | 4 +++-
M lib/sup/textfield.rb | 77 ++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
A lib/sup/util/ncurses.rb | 274 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M sup.gemspec | 2 +-
15 files changed, 435 insertions(+), 139 deletions(-)
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
@@ -6,6 +6,7 @@ Hamish Downer <dmishd at the gmail dot coms>
 Damien Leone <damien.leone at the fensalir dot frs>
 Sascha Silbe <sascha-pgp at the silbe dot orgs>
 Eric Weikl <eric.weikl at the gmx dot nets>
+Paweł Wilk <siefca at the gnu dot orgs>
 Ismo Puustinen <ismo at the iki dot fis>
 Nicolas Pouillard <nicolas.pouillard at the gmail dot coms>
 Michael Stapelberg <michael at the stapelberg dot des>
@@ -29,43 +30,44 @@ Marc Hartstein <marc.hartstein at the alum.vassar dot edus>
 Israel Herraiz <israel.herraiz at the gmail dot coms>
 Bo Borgerson <gigabo at the gmail dot coms>
 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>
+William Erik Baxter <web at the superscript dot coms>
 Grant Hollingworth <grant at the antiflux dot orgs>
 Adeodato Simó <dato at the net.com.org dot ess>
-Ico Doornekamp <ico at the pruts dot nls>
 Markus Klinik <markus.klinik at the gmx dot des>
+Ico Doornekamp <ico at the pruts dot nls>
 Daniel Schoepe <daniel.schoepe at the googlemail dot coms>
 James Taylor <james at the jamestaylor dot orgs>
 Jason Petsod <jason at the petsod 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>
+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>
-Giorgio Lando <patroclo7 at the gmail dot coms>
 Kevin Riggle <kevinr at the free-dissociation dot coms>
+Giorgio Lando <patroclo7 at the gmail 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>
-Adam Lloyd <adam at the alloy-d dot nets>
 Gregor Hoffleit <gregor at the sam.mediasupervision dot des>
 0xACE <0xACE at the users.noreply.github dot coms>
-Per Andersson <avtobiff at the gmail dot coms>
-MichaelRevell <mikearevell 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>
+Per Andersson <avtobiff at the gmail dot coms>
 Steven Walter <swalter at the monarch.(none)>
-Matthias Vallentin <vallentin at the icir dot orgs>
-akojo <atte.kojo at the gmail dot coms>
 Jon M. Dugan <jdugan at the es dot nets>
+Matthias Vallentin <vallentin at the icir dot orgs>
 Horacio Sanson <horacio at the skillupjapan.co dot jps>
 Stefan Lundström <lundst at the snabb.(none)>
+akojo <atte.kojo at the gmail dot coms>
+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.15.2 / 2013-12-20
+
+* Use the form_driver_w routine for inputing multibyte chars when
+  available.
+* Add hidden_alternates configuration option: hidden aliases for the
+  account.
+
 == 0.15.1 / 2013-12-04
 
 * Thread children are sorted last-activity latest (bottom).
diff --git a/ReleaseNotes b/ReleaseNotes
@@ -1,3 +1,7 @@
+Release 0.15.2:
+
+Use form_driver_w when available. New hidden_alternates option.
+
 Release 0.15.1:
 
 Sort threads last-activity-first and bug fix.
diff --git a/bin/sup b/bin/sup
@@ -4,9 +4,10 @@
 $:.unshift File.join(File.dirname(__FILE__), *%w[.. lib])
 
 require 'rubygems'
-
 require 'ncursesw'
 
+require 'sup/util/ncurses'
+
 no_gpgme = false
 begin
   require 'gpgme'
@@ -136,6 +137,7 @@ def start_cursing
   Ncurses.use_default_colors
   Ncurses.curs_set 0
   Ncurses.start_color
+  Ncurses.prepare_form_driver
   $cursing = true
 end
 
@@ -236,14 +238,14 @@ begin
 
   until Redwood::exceptions.nonempty? || $die
     c = begin
-      Ncurses.nonblocking_getch
+      Ncurses::CharCode.get false
     rescue Interrupt
       raise if BufferManager.ask_yes_or_no "Die ungracefully now?"
       BufferManager.draw_screen
-      nil
+      Ncurses::CharCode.empty
     end
 
-    if c.nil?
+    if c.empty?
       if BufferManager.sigwinch_happened?
         debug "redrawing screen on sigwinch"
         BufferManager.completely_redraw_screen
@@ -253,7 +255,7 @@ begin
 
     IdleManager.ping
 
-    if c == 410
+    if c.is_keycode? 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
diff --git a/lib/sup.rb b/lib/sup.rb
@@ -357,6 +357,7 @@ EOM
             :name => name.dup.fix_encoding!,
             :email => email.dup.fix_encoding!,
             :alternates => [],
+            :hidden_alternates => [],
             :sendmail => "/usr/sbin/sendmail -oem -ti",
             :signature => File.join(ENV["HOME"], ".signature"),
             :gpgkey => ""
diff --git a/lib/sup/account.rb b/lib/sup/account.rb
@@ -31,6 +31,8 @@ class AccountManager
 
   def initialize accounts
     @email_map = {}
+    @hidden_email_map = {}
+    @email_map_dirty = false
     @accounts = {}
     @regexen = {}
     @default_account = nil
@@ -40,7 +42,7 @@ class AccountManager
   end
 
   def user_accounts; @accounts.keys; end
-  def user_emails; @email_map.keys.select { |e| String === e }; end
+  def user_emails(type = :all); email_map(type).keys.select { |e| String === e }; end
 
   ## must be called first with the default account. fills in missing
   ## values from the default account.
@@ -50,7 +52,9 @@ class AccountManager
       [:name, :sendmail, :signature, :gpgkey].each { |k| hash[k] ||= @default_account.send(k) }
     end
     hash[:alternates] ||= []
+    hash[:hidden_alternates] ||= []
     fail "alternative emails are not an array: #{hash[:alternates]}" unless hash[:alternates].kind_of? Array
+    fail "hidden alternative emails are not an array: #{hash[:hidden_alternates]}" unless hash[:hidden_alternates].kind_of? Array
 
     [:name, :signature].each { |x| hash[x] ? hash[x].fix_encoding! : nil }
 
@@ -63,8 +67,11 @@ class AccountManager
     end
 
     ([hash[:email]] + hash[:alternates]).each do |email|
-      next if @email_map.member? email
-      @email_map[email] = a
+      add_email_to_map(:shown, email, a)
+    end
+
+    hash[:hidden_alternates].each do |email|
+      add_email_to_map(:hidden, email, a)
     end
 
     hash[:regexen].each do |re|
@@ -72,19 +79,44 @@ class AccountManager
     end if hash[:regexen]
   end
 
-  def is_account? p; is_account_email? p.email end
+  def is_account? p;    is_account_email? p.email       end
   def is_account_email? email; !account_for(email).nil? end
+
   def account_for email
-    if(a = @email_map[email])
-      a
-    else
-      @regexen.argfind { |re, a| re =~ email && a }
-    end
+    a = email_map[email]
+    a.nil? ? @regexen.argfind { |re, a| re =~ email && a } : a
   end
+
   def full_address_for email
     a = account_for email
     Person.full_address a.name, email
   end
-end
+
+  private
+
+  def add_email_to_map(type, email, acc)
+    type = :shown if type != :hidden
+    m = email_map(type)
+    unless m.member? email
+      m[email] = acc
+      @email_map_dirty = true
+    end
+  end
+
+  def email_map(type = nil)
+    case type
+    when :shown, :public  then @email_map
+    when :hidden          then @hidden_email_map
+    else
+      if @email_map_dirty
+        @email_map_all = @hidden_email_map.merge(@email_map)
+        if @email_map_all.count != @email_map.count + @hidden_email_map.count
+          @hidden_email_map.reject! { |m| @email_map.member? m }
+        end
+      end
+      @email_map_all ||= {}
+    end
+  end
+end # class AccountManager
 
 end
diff --git a/lib/sup/buffer.rb b/lib/sup/buffer.rb
@@ -2,67 +2,9 @@
 
 require 'etc'
 require 'thread'
-
 require 'ncursesw'
 
-if defined? Ncurses
-module Ncurses
-  def rows
-    lame, lamer = [], []
-    stdscr.getmaxyx lame, lamer
-    lame.first
-  end
-
-  def cols
-    lame, lamer = [], []
-    stdscr.getmaxyx lame, lamer
-    lamer.first
-  end
-
-  def curx
-    lame, lamer = [], []
-    stdscr.getyx lame, lamer
-    lamer.first
-  end
-
-  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
-    ## 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)
-      if Redwood::BufferManager.shelled?
-        # If we get input while we're shelled, we'll ignore it for the
-        # moment and use Ncurses.sync to wait until the shell_out is done.
-        Ncurses.sync { nil }
-      else
-        Ncurses.getch
-      end
-    end
-  end
-
-  ## pretends ctrl-c's are ctrl-g's
-  def safe_nonblocking_getch
-    nonblocking_getch
-  rescue Interrupt
-    KEY_CANCEL
-  end
-
-  module_function :rows, :cols, :curx, :nonblocking_getch, :safe_nonblocking_getch, :mutex, :sync
-
-  remove_const :KEY_ENTER
-  remove_const :KEY_CANCEL
-
-  KEY_ENTER = 10
-  KEY_CANCEL = 7 # ctrl-g
-  KEY_TAB = 9
-end
-end
+require 'sup/util/ncurses'
 
 module Redwood
 
@@ -214,7 +156,14 @@ EOS
     @sigwinch_mutex = Mutex.new
   end
 
-  def sigwinch_happened!; @sigwinch_mutex.synchronize { @sigwinch_happened = true } end
+  def sigwinch_happened!
+    @sigwinch_mutex.synchronize do
+      return if @sigwinch_happened
+      @sigwinch_happened = true
+      Ncurses.ungetch ?\C-l.ord
+    end
+  end
+
   def sigwinch_happened?; @sigwinch_mutex.synchronize { @sigwinch_happened } end
 
   def buffers; @name_map.to_a; end
@@ -266,7 +215,7 @@ EOS
 
   def handle_input c
     if @focus_buf
-      if @focus_buf.mode.in_search? && c != CONTINUE_IN_BUFFER_SEARCH_KEY.ord
+      if @focus_buf.mode.in_search? && c != CONTINUE_IN_BUFFER_SEARCH_KEY
         @focus_buf.mode.cancel_search!
         @focus_buf.mark_dirty
       end
@@ -398,9 +347,9 @@ EOS
     draw_screen
 
     until mode.done?
-      c = Ncurses.safe_nonblocking_getch
-      next unless c # getch timeout
-      break if c == Ncurses::KEY_CANCEL
+      c = Ncurses::CharCode.get
+      next unless c.present? # getch timeout
+      break if c.is_keycode? Ncurses::KEY_CANCEL
       begin
         mode.handle_input c
       rescue InputSequenceAborted # do nothing
@@ -590,8 +539,8 @@ EOS
     end
 
     while true
-      c = Ncurses.safe_nonblocking_getch
-      next unless c # getch timeout
+      c = Ncurses::CharCode.get
+      next unless c.present? # getch timeout
       break unless tf.handle_input c # process keystroke
 
       if tf.new_completions?
@@ -643,10 +592,11 @@ EOS
     ret = nil
     done = false
     until done
-      key = Ncurses.safe_nonblocking_getch or next
-      if key == Ncurses::KEY_CANCEL
+      key = Ncurses::CharCode.get
+      next if key.empty?
+      if key.is_keycode? Ncurses::KEY_CANCEL
         done = true
-      elsif accept.nil? || accept.empty? || accept.member?(key)
+      elsif accept.nil? || accept.empty? || accept.member?(key.code)
         ret = key
         done = true
       end
@@ -664,7 +614,7 @@ EOS
   ## returns true (y), false (n), or nil (ctrl-g / cancel)
   def ask_yes_or_no question
     case(r = ask_getch question, "ynYN")
-    when ?y.ord, ?Y.ord
+    when ?y, ?Y
       true
     when nil
       nil
@@ -683,7 +633,7 @@ EOS
     action, text = keymap.action_for c
     while action.is_a? Keymap # multi-key commands, prompt
       key = BufferManager.ask_getch text
-      unless key # user canceled, abort
+      unless key.empty? # user canceled, abort
         erase_flash
         raise InputSequenceAborted
       end
diff --git a/lib/sup/index.rb b/lib/sup/index.rb
@@ -134,7 +134,7 @@ EOS
 
   def add_message m; sync_message m, true end
   def update_message m; sync_message m, true end
-  def update_message_state m; sync_message m, false end
+  def update_message_state m; sync_message m[0], false, m[1] end
 
   def save_index
     info "Flushing Xapian updates to disk. This may take a while..."
@@ -530,18 +530,18 @@ EOS
     query
   end
 
-  def save_message m
+  def save_message m, sync_back = true
     if @sync_worker
-      @sync_queue << m
+      @sync_queue << [m, sync_back]
     else
-      update_message_state m
+      update_message_state [m, sync_back]
     end
     m.clear_dirty
   end
 
-  def save_thread t
+  def save_thread t, sync_back = true
     t.each_dirty_message do |m|
-      save_message m
+      save_message m, sync_back
     end
   end
 
@@ -676,10 +676,10 @@ EOS
     end
   end
 
-  def sync_message m, overwrite
+  def sync_message m, overwrite, sync_back = true
     ## TODO: we should not save the message if the sync_back failed
     ## since it would overwrite the location field
-    m.sync_back
+    m.sync_back if sync_back
 
     doc = synchronize { find_doc(m.id) }
     existed = doc != nil
diff --git a/lib/sup/keymap.rb b/lib/sup/keymap.rb
@@ -1,3 +1,5 @@
+require 'sup/util/ncurses'
+
 module Redwood
 
 class Keymap
@@ -96,11 +98,11 @@ EOS
   end
 
   def action_for kc
-    action, help, keys = @map[kc]
+    action, help, keys = @map[kc.code]
     [action, help]
   end
 
-  def has_key? k; @map[k] end
+  def has_key? k; @map[k.code] end
 
   def keysyms; @map.values.map { |action, help, keys| keys }.flatten; end
 
diff --git a/lib/sup/message_chunks.rb b/lib/sup/message_chunks.rb
@@ -205,6 +205,7 @@ EOS
       begin
         file = Tempfile.new(["sup", Shellwords.escape(@filename.gsub("/", "_")) || "sup-attachment"])
         file.print @raw_content
+        file.flush
         yield file if block_given?
         return file.path
       ensure
diff --git a/lib/sup/modes/thread_index_mode.rb b/lib/sup/modes/thread_index_mode.rb
@@ -207,7 +207,7 @@ EOS
       @ts.delete_message m
       @ts.add_message m
     end
-    Index.save_thread t
+    Index.save_thread t, sync_back = false
     update_text_for_line l
   end
 
diff --git a/lib/sup/tagger.rb b/lib/sup/tagger.rb
@@ -1,3 +1,5 @@
+require 'sup/util/ncurses'
+
 module Redwood
 
 class Tagger
@@ -27,7 +29,7 @@ class Tagger
 
     unless action
       c = BufferManager.ask_getch "apply to #{num_tagged} tagged #{noun}:"
-      return if c.nil? # user cancelled
+      return if c.empty? # user cancelled
       action = @mode.resolve_input c
     end
 
diff --git a/lib/sup/textfield.rb b/lib/sup/textfield.rb
@@ -1,3 +1,5 @@
+require 'sup/util/ncurses'
+
 module Redwood
 
 ## a fully-functional text field supporting completions, expansions,
@@ -16,6 +18,8 @@ module Redwood
 ## in sup, completion support is implemented through BufferManager#ask
 ## and CompletionMode.
 class TextField
+  include Ncurses::Form::DriverHelpers
+
   def initialize
     @i = nil
     @history = []
@@ -48,8 +52,8 @@ class TextField
     @w.attrset Colormap.color_for(:none)
     @w.mvaddstr @y, 0, @question
     Ncurses.curs_set 1
-    Ncurses::Form.form_driver @form, Ncurses::Form::REQ_END_FIELD
-    Ncurses::Form.form_driver @form, Ncurses::Form::REQ_NEXT_CHAR if @value && @value =~ / $/ # fucking RETARDED
+    form_driver_key Ncurses::Form::REQ_END_FIELD
+    form_driver_key Ncurses::Form::REQ_NEXT_CHAR if @value && @value =~ / $/ # fucking RETARDED
   end
 
   def deactivate
@@ -63,7 +67,7 @@ class TextField
 
   def handle_input c
     ## short-circuit exit paths
-    case c
+    case c.code
     when Ncurses::KEY_ENTER # submit!
       @value = get_cursed_value
       @history.push @value unless @value =~ /^\s*$/
@@ -97,39 +101,27 @@ class TextField
     reset_completion_state
     @value = nil
 
-    d =
-      case c
+    # ctrl_c: control char
+    ctrl_c =
+      case c.keycode # only test for keycodes
       when Ncurses::KEY_LEFT
         Ncurses::Form::REQ_PREV_CHAR
       when Ncurses::KEY_RIGHT
         Ncurses::Form::REQ_NEXT_CHAR
       when Ncurses::KEY_DC
         Ncurses::Form::REQ_DEL_CHAR
-      when Ncurses::KEY_BACKSPACE, 127 # 127 is also a backspace keysym
+      when Ncurses::KEY_BACKSPACE
         Ncurses::Form::REQ_DEL_PREV
-      when ?\C-a.ord, Ncurses::KEY_HOME
+      when Ncurses::KEY_HOME
         nop
         Ncurses::Form::REQ_BEG_FIELD
-      when ?\C-e.ord, Ncurses::KEY_END
+      when Ncurses::KEY_END
         Ncurses::Form::REQ_END_FIELD
-      when ?\C-k.ord
-        Ncurses::Form::REQ_CLR_EOF
-      when ?\C-u.ord
-        set_cursed_value cursed_value_after_point
-        Ncurses::Form.form_driver @form, Ncurses::Form::REQ_END_FIELD
-        nop
-        Ncurses::Form::REQ_BEG_FIELD
-      when ?\C-w.ord
-        while action = remove_extra_space
-          Ncurses::Form.form_driver @form, action
-        end
-        Ncurses::Form.form_driver @form, Ncurses::Form::REQ_PREV_CHAR
-        Ncurses::Form.form_driver @form, Ncurses::Form::REQ_DEL_WORD
       when Ncurses::KEY_UP, Ncurses::KEY_DOWN
         unless !@i || @history.empty?
           value = get_cursed_value
           #debug "history before #{@history.inspect}"
-          @i = @i + (c == Ncurses::KEY_UP ? -1 : 1)
+          @i = @i + (c.is_keycode?(Ncurses::KEY_UP) ? -1 : 1)
           @i = 0 if @i < 0
           @i = @history.size if @i > @history.size
           @value = @history[@i] || ''
@@ -138,10 +130,37 @@ class TextField
           Ncurses::Form::REQ_END_FIELD
         end
       else
-        c
+        # return other keycode or nil if it's not a keycode
+        c.dumb? ? nil : c.keycode
       end
 
-    Ncurses::Form.form_driver @form, d if d
+    # handle keysyms
+    # ctrl_c: control char
+    ctrl_c = case c
+      when ?\177                          # backspace (octal)
+        Ncurses::Form::REQ_DEL_PREV
+      when ?\C-a                          # home
+        nop
+        Ncurses::Form::REQ_BEG_FIELD
+      when ?\C-e                          # end keysym
+        Ncurses::Form::REQ_END_FIELD
+      when ?\C-k
+        Ncurses::Form::REQ_CLR_EOF
+      when ?\C-u
+        set_cursed_value cursed_value_after_point
+        form_driver_key Ncurses::Form::REQ_END_FIELD
+        nop
+        Ncurses::Form::REQ_BEG_FIELD
+      when ?\C-w
+        while action = remove_extra_space
+          form_driver_key action
+        end
+        form_driver_key Ncurses::Form::REQ_PREV_CHAR
+        form_driver_key Ncurses::Form::REQ_DEL_WORD
+      end if ctrl_c.nil?
+
+    c.replace(ctrl_c).keycode! if ctrl_c  # no effect for dumb CharCode
+    form_driver c if c.present?
     true
   end
 
@@ -159,7 +178,7 @@ private
     return nil unless @field
 
     x = Ncurses.curx
-    Ncurses::Form.form_driver @form, Ncurses::Form::REQ_VALIDATION
+    form_driver_key Ncurses::Form::REQ_VALIDATION
     v = @field.field_buffer(0).gsub(/^\s+|\s+$/, "")
 
     ## cursor <= end of text
@@ -175,7 +194,7 @@ private
     # system locale and also hopefully the terminal/input encoding. an
     # incorrectly configured terminal encoding (not matching the system
     # encoding) will produce erronous results, but will also do that for
-    # a log of other programs since it is impossible to detect which is
+    # a lot of other programs since it is impossible to detect which is
     # which and what encoding the inputted byte chars are supposed to have.
     v.force_encoding($encoding).fix_encoding!
   end
@@ -183,7 +202,7 @@ private
   def remove_extra_space
     return nil unless @field
 
-    Ncurses::Form.form_driver @form, Ncurses::Form::REQ_VALIDATION
+    form_driver_key Ncurses::Form::REQ_VALIDATION
     x = Ncurses.curx
     v = @field.field_buffer(0).gsub(/^\s+|\s+$/, "")
     v_index = x - @question.length
@@ -226,8 +245,8 @@ private
   ## this is almost certainly unnecessary, but it's the only way
   ## i could get ncurses to remember my form's value
   def nop
-    Ncurses::Form.form_driver @form, " ".ord
-    Ncurses::Form.form_driver @form, Ncurses::Form::REQ_DEL_PREV
+    form_driver_char " "
+    form_driver_key Ncurses::Form::REQ_DEL_PREV
   end
 end
 end
diff --git a/lib/sup/util/ncurses.rb b/lib/sup/util/ncurses.rb
@@ -0,0 +1,274 @@
+require 'ncursesw'
+require 'sup/util'
+
+if defined? Ncurses
+module Ncurses
+
+  ## Helper class for storing keycodes
+  ## and multibyte characters.
+  class CharCode < String
+    ## Status code allows us to detect
+    ## printable characters and control codes.
+    attr_reader :status
+
+    ## Reads character from user input.
+    def self.nonblocking_getwch
+      # If we get input while we're shelled, we'll ignore it for the
+      # moment and use Ncurses.sync to wait until the shell_out is done.
+      begin
+        s, c = Redwood::BufferManager.shelled? ? Ncurses.sync { nil } : Ncurses.get_wch
+        break if s != Ncurses::ERR
+      end until IO.select([$stdin], nil, nil, 2)
+      [s, c]
+    end
+
+    ## Returns empty singleton.
+    def self.empty
+      Empty.instance
+    end
+
+    ## Creates new instance of CharCode
+    ## that keeps a given keycode.
+    def self.keycode(c)
+      generate c, Ncurses::KEY_CODE_YES
+    end
+
+    ## Creates new instance of CharCode
+    ## that keeps a printable character.
+    def self.character(c)
+      generate c, Ncurses::OK
+    end
+
+    ## Generates new object like new
+    ## but for empty or erroneous objects
+    ## it returns empty singleton.
+    def self.generate(c = nil, status = Ncurses::OK)
+      if status == Ncurses::ERR || c.nil? || c === Ncurses::ERR
+        empty
+      else
+        new(c, status)
+      end
+    end
+
+    ## Gets character from input.
+    ## Pretends ctrl-c's are ctrl-g's.
+    def self.get handle_interrupt=true
+      begin
+        status, code = nonblocking_getwch
+        generate code, status
+      rescue Interrupt => e
+        raise e unless handle_interrupt
+        keycode Ncurses::KEY_CANCEL
+      end
+    end
+
+    ## Enables dumb mode for any new instance.
+    def self.dumb!
+      @dumb = true
+    end
+
+    ## Asks if dumb mode was set
+    def self.dumb?
+      defined?(@dumb) && @dumb
+    end
+
+    def initialize(c = "", status = Ncurses::OK)
+      @status = status
+      c = "" if c.nil?
+      return super("") if status == Ncurses::ERR
+      c = enc_char(c) if c.is_a?(Fixnum)
+      super c.length > 1 ? c[0,1] : c
+    end
+
+    ## Proxy method for String's replace
+    def replace(c)
+      return self if c.object_id == object_id
+      if c.is_a?(self.class)
+        @status = c.status
+        super(c)
+      else
+        @status = Ncurses::OK
+        c = "" if c.nil?
+        c = enc_char(c) if c.is_a?(Fixnum)
+        super c.length > 1 ? c[0,1] : c
+      end
+    end
+
+    def to_character    ; character? ? self : "<#{code}>"           end  ## Returns character or code as a string
+    def to_keycode      ; keycode?   ? code : Ncurses::ERR          end  ## Returns keycode or ERR if it's not a keycode
+    def to_sequence     ; bytes.to_a                                end  ## Returns unpacked sequence of bytes for a character
+    def code            ; ord                                       end  ## Returns decimal representation of a character
+    def is_keycode?(c)  ; keycode?   &&  code == c                  end  ## Tests if keycode matches
+    def is_character?(c); character? &&  self == c                  end  ## Tests if character matches
+    def try_keycode     ; keycode?   ? code : nil                   end  ## Returns dec. code if keycode, nil otherwise
+    def try_character   ; character? ? self : nil                   end  ## Returns character if character, nil otherwise
+    def keycode         ; try_keycode                               end  ## Alias for try_keycode
+    def character       ; try_character                             end  ## Alias for try_character
+    def character?      ; dumb? || @status == Ncurses::OK           end  ## Returns true if character
+    def character!      ; @status  = Ncurses::OK ; self             end  ## Sets character flag
+    def keycode?        ; dumb? || @status == Ncurses::KEY_CODE_YES end  ## Returns true if keycode
+    def keycode!        ; @status  = Ncurses::KEY_CODE_YES ; self   end  ## Sets keycode flag
+    def keycode=(c)     ; replace(c); keycode! ; self               end  ## Sets keycode    
+    def present?        ; not empty?                                end  ## Proxy method
+    def printable?      ; character?                                end  ## Alias for character?
+    def dumb?           ; self.class.dumb?                          end  ## True if we cannot distinguish keycodes from characters
+
+    # Empty singleton that
+    # keeps GC from going crazy.
+    class Empty < CharCode
+      include Singleton
+
+      ## Wrap methods that may change us
+      ## and generate new object instead.
+      [ :"[]=", :"<<", :replace, :insert, :prepend, :append, :concat, :force_encoding, :setbyte ].
+      select{ |m| public_method_defined?(m) }.
+      concat(public_instance_methods.grep(/!\z/)).
+      each do |m|
+        class_eval <<-EVAL
+          def #{m}(*args)
+            CharCode.new.#{m}(*args)
+          end
+        EVAL
+      end
+
+      ## proxy with class-level instance variable delegation
+      def self.dumb?
+        superclass.dumb? or !!@dumb
+      end
+
+      def self.empty
+        instance
+      end
+
+      def initialize
+        super("", Ncurses::ERR)
+      end
+
+      def empty?    ; true  end   ## always true
+      def present?  ; false end   ## always false
+      def clear     ; self  end   ## always self
+
+      self
+    end.init # CharCode::Empty
+
+    private
+
+    ## Tries to make external character right.
+    def enc_char(c)
+      begin
+        character = c.chr($encoding)
+      rescue RangeError, ArgumentError
+        begin
+          character = [c].pack('U')
+        rescue RangeError
+          begin
+            character = c.chr
+          rescue
+            begin
+              character = [c].pack('C')
+            rescue
+              character = ""
+              @status = Ncurses::ERR
+            end
+          end
+        end
+        character.fix_encoding!
+      end
+    end
+  end # class CharCode
+
+  def rows
+    lame, lamer = [], []
+    stdscr.getmaxyx lame, lamer
+    lame.first
+  end
+
+  def cols
+    lame, lamer = [], []
+    stdscr.getmaxyx lame, lamer
+    lamer.first
+  end
+
+  def curx
+    lame, lamer = [], []
+    stdscr.getyx lame, lamer
+    lamer.first
+  end
+
+  ## Create replacement wrapper for form_driver_w (), which is not (yet) a standard
+  ## function in ncurses. Some systems (Mac OS X) does not have a working
+  ## form_driver that accepts wide chars. We are just falling back to form_driver, expect problems.
+  def prepare_form_driver
+    if not defined? Form.form_driver_w
+      warn "Your Ncursesw does not have a form_driver_w function (wide char aware), " \
+           "non-ASCII chars may not work on your system."
+      Form.module_eval <<-FRM_DRV, __FILE__, __LINE__ + 1
+        def form_driver_w form, status, c
+          form_driver form, c
+        end
+        module_function :form_driver_w
+        module DriverHelpers
+          def form_driver c
+            if !c.dumb? && c.printable?
+              c.each_byte do |code|
+                Ncurses::Form.form_driver @form, code
+              end
+            else
+              Ncurses::Form.form_driver @form, c.code
+            end
+          end
+        end
+      FRM_DRV
+    end # if not defined? Form.form_driver_w
+    if not defined? Ncurses.get_wch
+      warn "Your Ncursesw does not have a get_wch function (wide char aware), " \
+           "non-ASCII chars may not work on your system."
+      Ncurses.module_eval <<-GET_WCH, __FILE__, __LINE__ + 1
+        def get_wch
+          c = getch
+          c == Ncurses::ERR ? [c, 0] : [Ncurses::OK, c]
+        end
+        module_function :get_wch
+      GET_WCH
+      CharCode.dumb!
+    end # if not defined? Ncurses.get_wch
+  end
+
+  def mutex; @mutex ||= Mutex.new; end
+  def sync &b; mutex.synchronize(&b); end
+
+  module_function :rows, :cols, :curx, :mutex, :sync, :prepare_form_driver
+
+  remove_const :KEY_ENTER
+  remove_const :KEY_CANCEL
+
+  KEY_ENTER = 10
+  KEY_CANCEL = 7 # ctrl-g
+  KEY_TAB = 9
+
+  module Form
+    ## This module contains helpers that ease
+    ## using form_driver_ methods when @form is present.
+    module DriverHelpers
+      private
+
+      ## Ncurses::Form.form_driver_w wrapper for keycodes and control characters.
+      def form_driver_key c
+        form_driver CharCode.keycode(c)
+      end
+
+      ## Ncurses::Form.form_driver_w wrapper for printable characters.
+      def form_driver_char c
+        form_driver CharCode.character(c)
+        #c.is_a?(Fixnum) ? c : c.ord
+      end
+
+      ## Ncurses::Form.form_driver_w wrapper for charcodes.
+      def form_driver c
+        Ncurses::Form.form_driver_w @form, c.status, c.code
+      end
+    end # module DriverHelpers
+  end # module Form
+
+end # module Ncurses
+end # if defined? Ncurses
diff --git a/sup.gemspec b/sup.gemspec
@@ -48,7 +48,7 @@ SUP: If you are upgrading Sup from before version 0.14.0: Please
     s.required_ruby_version = '>= 1.9.2'
 
     s.add_runtime_dependency "xapian-ruby", "~> 1.2.15"
-    s.add_runtime_dependency "ncursesw-sup", "~> 1.3.1"
+    s.add_runtime_dependency "ncursesw", "~> 1.4.0"
     s.add_runtime_dependency "rmail-sup", "~> 1.0.1"
     s.add_runtime_dependency "highline"
     s.add_runtime_dependency "trollop", ">= 1.12"