sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/
commit 7e7858c8e731daec7ab7ac758bff7a1ee884945c
parent a6977bed247f428072ce2bf82b6dd596526e50d7
Author: Paweł Wilk <siefca@gnu.org>
Date:   Mon, 16 Dec 2013 15:34:44 +0100

Merge #183: Wrap form_driver_w for wide char input

form_driver does not work correctly on all systems (specifically Mac OS X),
it is difficult or impossible to determine whether the input char is a
char or key code resulting from a char sequence in a multibyte char.

If no form_driver_w is present we fall back to form_driver.
form_driver_w is included in ncursesw-1.4.2 with the Ncurses 5.9 20131207
patchlevel.

http://lists.gnu.org/archive/html/bug-ncurses/2013-12/msg00010.html

original patch:

http://lists.gnu.org/archive/html/bug-ncurses/2013-11/msg00015.html

Author: Paweł Wilk 

Squashed commit of the following:

commit 7a513f25d755523576268bfa5c77fafc838564b6
Author: Paweł Wilk 
Date:   Tue Nov 26 19:49:30 2013 +0100

    Added conditional select in Ncurses::nonblocking_getwch to prevent erroneous CharCode spawning

commit 0b6859eb1b5b02e98f07214e45baab550e6f07e7
Author: Paweł Wilk 
Date:   Sun Nov 24 21:14:52 2013 +0100

    BufferManager.sigwinch_happened! modified to run once if it already happened

commit 857cb60058f966e22bd0f06950c5b8fa3d8e622f
Author: Paweł Wilk 
Date:   Sun Nov 24 20:44:21 2013 +0100

    Added workaround for screen redrawing when sigwinch happened

commit d72d72fb27b23af8d386faa36e1d78c0087c1115
Author: Paweł Wilk 
Date:   Sun Nov 24 20:43:46 2013 +0100

    Optimizations in Ncurses::CharCode.get

commit c2139b6df4aba9551adf4275cb1d8bc70e4727e2
Author: Paweł Wilk 
Date:   Sun Nov 24 18:19:57 2013 +0100

    Fixed uninitialized instance variable warning

commit 72fb7e43b7237ebc582ca58fe2df115ea3747ef3
Author: Paweł Wilk 
Date:   Sun Nov 24 17:43:16 2013 +0100

    Added critical patches for Ncurses::Form::DriverHelpers#form_driver and Ncurses.get_wch

commit 340fb7f249b799cafb2b1e40f8b8b31e82f449d2
Author: Paweł Wilk 
Date:   Sun Nov 24 17:42:24 2013 +0100

    Ncurses::Form::DriverHelpers improved; now all wrappers are reusing core wrapper

commit ca362c294c08cd7ee81bf52722c819c4c31c6a04
Author: Paweł Wilk 
Date:   Sun Nov 24 17:40:38 2013 +0100

    Ncurses::CharCode#enc_char improved

commit 380079ea0627c9f1440b5097563e6dece352cae4
Author: Paweł Wilk 
Date:   Sun Nov 24 17:40:01 2013 +0100

    Added Ncurses::CharCode::Empty.empty override

commit 8da83d9952fd6020175ac4b89e404adc5a72d6fe
Author: Paweł Wilk 
Date:   Sun Nov 24 17:39:28 2013 +0100

    Cleanups

commit de9f7bc1b2cd5d71d870c02d0fe2242786f00445
Author: Paweł Wilk 
Date:   Sun Nov 24 17:39:02 2013 +0100

    Added Ncurses::CharCode.dumb! switch to control behavior when CharCode cannot detect keycodes

commit ffad7e5e9d6951ae0834be9c453b91e305dbbcdf
Author: Paweł Wilk 
Date:   Sun Nov 24 17:35:30 2013 +0100

    Added Ncurses::CharCode.generate to save objects from spawning when ERR is returned by get(w)ch

commit ad6f6f2c0c8020ff3b5affe242c40972d6a3a4ba
Author: Paweł Wilk 
Date:   Sun Nov 24 03:02:01 2013 +0100

    Removed printing of warning about a missing form_driver_w() function

commit ea277c19791330943a3696cb97d9a6ad99454d80
Author: Paweł Wilk 
Date:   Sun Nov 24 02:55:23 2013 +0100

    Cleanups

commit 505532e288072be92ade36bec94555044dce9b2d
Author: Paweł Wilk 
Date:   Sun Nov 24 01:24:08 2013 +0100

    TextField now uses form_driver helpers from Ncurses::Form::DriverHelpers

commit 91037291c28f00cee2da5ea505657aa814ddcffc
Author: Paweł Wilk 
Date:   Sun Nov 24 01:23:22 2013 +0100

    Added Form::DriverHelpers module with methods that ease usage of form_driver

commit 05c2b5611d5e96d68e4685c4c27a5d267cde4a48
Author: Paweł Wilk 
Date:   Sun Nov 24 01:22:08 2013 +0100

    Improved Ncurses::CharCode#replace method (detects if input is a charcode or a character)

commit a2f0dac449bc743236f3925b35aa6de43fc189d0
Author: Paweł Wilk 
Date:   Sun Nov 24 01:21:11 2013 +0100

    Removed previous wrapper for form_driver_w()

commit 3a5cbdc8c1941745bcd1498644be02e2487e7905
Author: Paweł Wilk 
Date:   Sun Nov 24 01:20:12 2013 +0100

    Cleanups

commit a9585535f6b647c3094f5f4485f8128f127c4357
Author: Paweł Wilk 
Date:   Sun Nov 24 01:19:51 2013 +0100

    Replacement wrapper for form_driver_w() moved to Ncurses::prepare_form_driver and called after initscr

commit 7766cbfb20941dd6d67456008af1125b99d6a6be
Author: Paweł Wilk 
Date:   Sun Nov 24 01:17:37 2013 +0100

    Multiple typos fixed

commit 8bf705c135d9458be759cd72ebef7eef98085a8d
Author: Paweł Wilk 
Date:   Sat Nov 23 20:58:54 2013 +0100

    Key for continuing search in buffer in Buffer#handle_input is now compared directly

commit 1dcfabd56c423762734b4a7594d209ceeb07fe16
Author: Paweł Wilk 
Date:   Sat Nov 23 20:57:22 2013 +0100

    Moved ncurses tweaks to lib/sup/util/ncurses.rb, added CharCode::Empty singleton class

commit ed85b526c52b21c58b029921b51c0183aa747876
Author: Paweł Wilk 
Date:   Sat Nov 23 17:37:50 2013 +0100

    TextField class is now making use of Ncurses::Form.form_driver_w

commit 9ce61341efdba172fddf41b10e0277e646177208
Author: Paweł Wilk 
Date:   Sat Nov 23 17:37:10 2013 +0100

    Cleanups in buffer.rb

commit f8f7116c542de63c2ce3f04e96d18779fe28e3b0
Author: Paweł Wilk 
Date:   Sat Nov 23 17:36:49 2013 +0100

    Added compat proxy for the Ncurses::Form.form_driver_w module method

commit 171eada22b63b3e105eae4f80b157f0e2bdd0a0d
Author: Paweł Wilk 
Date:   Fri Nov 22 02:36:27 2013 +0100

    Fixed TextField#handle_input (keysyms taken into account) and prepared for form_driver workaroud

commit 3217a655a09fe6adb35049fdaa16af7e1788d0ee
Author: Paweł Wilk 
Date:   Fri Nov 22 02:32:28 2013 +0100

    Added Ncurses::curyx module method (helper for reading whole cursor position)

commit 60b02d6f6257601737086fddfcd33fba60d033a6
Author: Paweł Wilk 
Date:   Thu Nov 21 19:08:51 2013 +0100

    Ncurses::CharCode::get now uses chr to create character based on code returned by get_wch

commit 58088c4f5fa27dce2234c43d3c1beba7539ce87b
Author: Paweł Wilk 
Date:   Thu Nov 21 17:55:45 2013 +0100

    Typo fixed

commit ca8e98447c209680988b162e9f980ff69cd693db
Author: Paweł Wilk 
Date:   Thu Nov 21 17:21:49 2013 +0100

    Ncurses::CharCode class refactored, code adapted to use its instances

    Details:

    - changed Ncurses::CharCode to work with wide characters,
    - removed select wrapper when reading input,
    - moved input reading into Ncurses::CharCode class,
    - implemented passing Ncurses::CharCode objects as far as possible.

commit f0168bea3ada9cd5c3168c9fc324ed3aa458119e
Author: Paweł Wilk 
Date:   Tue Nov 19 23:20:13 2013 +0100

    Adapted Keymap, Buffer and TextField classes to make use of CharCode objects

commit dfea936ae8661e71b80618f44a8bd9f3ca68634c
Author: Paweł Wilk 
Date:   Tue Nov 19 23:17:11 2013 +0100

    Added Ncurses::CharCode helper class for storing and transferring wide characters

Diffstat:
M History.txt | 5 +++++
M ReleaseNotes | 4 ++++
M bin/sup | 12 +++++++-----
M lib/sup/buffer.rb | 92 ++++++++++++++++++-------------------------------------------------------------
M lib/sup/keymap.rb | 6 ++++--
M lib/sup/tagger.rb | 4 +++-
M lib/sup/textfield.rb | 75 +++++++++++++++++++++++++++++++++++++++++++++++----------------------------
A lib/sup/util/ncurses.rb | 274 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 365 insertions(+), 107 deletions(-)
diff --git a/History.txt b/History.txt
@@ -1,3 +1,8 @@
+== 0.15.2 /
+
+* Use the form_driver_w routine for inputing multibyte chars when
+  available.
+
 == 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.
+
 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/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/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/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
@@ -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