sup

A curses threads-with-tags style email client

sup.git

git clone https://supmua.dev/git/sup/

lib/sup/textfield.rb (7343B) - raw

      1 require 'sup/util/ncurses'
      2 
      3 module Redwood
      4 
      5 ## a fully-functional text field supporting completions, expansions,
      6 ## history--everything!
      7 ##
      8 ## writing this fucking sucked. if you thought ncurses was some 1970s
      9 ## before-people-knew-how-to-program bullshit, wait till you see
     10 ## ncurses forms.
     11 ##
     12 ## completion comments: completion is done emacs-style, and mostly
     13 ## depends on outside support, as we merely signal the existence of a
     14 ## new set of completions to show (#new_completions?)  or that the
     15 ## current list of completions should be rolled if they're too large
     16 ## to fill the screen (#roll_completions?).
     17 ##
     18 ## in sup, completion support is implemented through BufferManager#ask
     19 ## and CompletionMode.
     20 class TextField
     21   include Ncurses::Form::DriverHelpers
     22 
     23   def initialize
     24     @i = nil
     25     @history = []
     26 
     27     @completion_block = nil
     28     reset_completion_state
     29   end
     30 
     31   bool_reader :new_completions, :roll_completions
     32   attr_reader :completions
     33 
     34   def value; @value || get_cursed_value end
     35 
     36   def activate window, y, x, width, question, default=nil, &block
     37     @w, @y, @x, @width = window, y, x, width
     38     @question = question
     39     @completion_block = block
     40     @field = Ncurses::Form.new_field 1, @width - question.length, @y, @x + question.length, 0, 0
     41     if @field.respond_to? :opts_off
     42       @field.opts_off Ncurses::Form::O_STATIC
     43       @field.opts_off Ncurses::Form::O_BLANK
     44     end
     45     @form = Ncurses::Form.new_form [@field]
     46     @value = default || ''
     47     Ncurses::Form.post_form @form
     48     set_cursed_value @value
     49   end
     50 
     51   def position_cursor
     52     @w.attrset Colormap.color_for(:none)
     53     @w.mvaddstr @y, 0, @question
     54     Ncurses.curs_set 1
     55     form_driver_key Ncurses::Form::REQ_END_FIELD
     56     form_driver_key Ncurses::Form::REQ_NEXT_CHAR if @value && @value =~ / $/ # fucking RETARDED
     57   end
     58 
     59   def deactivate
     60     reset_completion_state
     61     @form.unpost_form
     62     @form.free_form
     63     @field.free_field
     64     @field = nil
     65     Ncurses.curs_set 0
     66   end
     67 
     68   def handle_input c
     69     ## short-circuit exit paths
     70     case c.code
     71     when Ncurses::KEY_ENTER # submit!
     72       @value = get_cursed_value
     73       @history.push @value unless @value =~ /^\s*$/
     74       @i = @history.size
     75       return false
     76     when Ncurses::KEY_CANCEL # cancel
     77       @value = nil
     78       return false
     79     when Ncurses::KEY_TAB # completion
     80       return true unless @completion_block
     81       if @completions.empty?
     82         v = get_cursed_value
     83         c = @completion_block.call v
     84         if c.size > 0
     85           @value = c.map { |full, short| full }.shared_prefix(true)
     86           set_cursed_value @value
     87           position_cursor
     88         end
     89         if c.size > 1
     90           @completions = c
     91           @new_completions = true
     92           @roll_completions = false
     93         end
     94       else
     95         @new_completions = false
     96         @roll_completions = true
     97       end
     98       return true
     99     end
    100 
    101     reset_completion_state
    102     @value = nil
    103 
    104     # ctrl_c: control char
    105     ctrl_c =
    106       case c.keycode # only test for keycodes
    107       when Ncurses::KEY_LEFT
    108         Ncurses::Form::REQ_PREV_CHAR
    109       when Ncurses::KEY_RIGHT
    110         Ncurses::Form::REQ_NEXT_CHAR
    111       when Ncurses::KEY_DC
    112         Ncurses::Form::REQ_DEL_CHAR
    113       when Ncurses::KEY_BACKSPACE
    114         Ncurses::Form::REQ_DEL_PREV
    115       when Ncurses::KEY_HOME
    116         nop
    117         Ncurses::Form::REQ_BEG_FIELD
    118       when Ncurses::KEY_END
    119         Ncurses::Form::REQ_END_FIELD
    120       when Ncurses::KEY_UP, Ncurses::KEY_DOWN
    121         unless !@i || @history.empty?
    122           #debug "history before #{@history.inspect}"
    123           @i = @i + (c.is_keycode?(Ncurses::KEY_UP) ? -1 : 1)
    124           @i = 0 if @i < 0
    125           @i = @history.size if @i > @history.size
    126           @value = @history[@i] || ''
    127           #debug "history after #{@history.inspect}"
    128           set_cursed_value @value
    129           Ncurses::Form::REQ_END_FIELD
    130         end
    131       else
    132         # return other keycode or nil if it's not a keycode
    133         c.dumb? ? nil : c.keycode
    134       end
    135 
    136     # handle keysyms
    137     # ctrl_c: control char
    138     ctrl_c = case c
    139       when ?\177                          # backspace (octal)
    140         Ncurses::Form::REQ_DEL_PREV
    141       when ?\C-a                          # home
    142         nop
    143         Ncurses::Form::REQ_BEG_FIELD
    144       when ?\C-e                          # end keysym
    145         Ncurses::Form::REQ_END_FIELD
    146       when ?\C-k
    147         Ncurses::Form::REQ_CLR_EOF
    148       when ?\C-u
    149         set_cursed_value cursed_value_after_point
    150         form_driver_key Ncurses::Form::REQ_END_FIELD
    151         nop
    152         Ncurses::Form::REQ_BEG_FIELD
    153       when ?\C-w
    154         while action = remove_extra_space
    155           form_driver_key action
    156         end
    157         form_driver_key Ncurses::Form::REQ_PREV_CHAR
    158         form_driver_key Ncurses::Form::REQ_DEL_WORD
    159       end if ctrl_c.nil?
    160 
    161     c.replace(ctrl_c).keycode! if ctrl_c  # no effect for dumb CharCode
    162     form_driver c if c.present?
    163     true
    164   end
    165 
    166 private
    167 
    168   def reset_completion_state
    169     @completions = []
    170     @new_completions = @roll_completions = @clear_completions = false
    171   end
    172 
    173   ## ncurses inanity wrapper
    174   ##
    175   ## DO NOT READ THIS CODE. YOU WILL GO MAD.
    176   def get_cursed_value
    177     return nil unless @field
    178 
    179     x = Ncurses.curx
    180     form_driver_key Ncurses::Form::REQ_VALIDATION
    181     v = @field.field_buffer(0).gsub(/^\s+|\s+$/, "")
    182 
    183     ## cursor <= end of text
    184     if x - @question.length - v.length <= 0
    185       v
    186     else # trailing spaces
    187       v + (" " * (x - @question.length - v.length))
    188     end
    189 
    190     # ncurses returns a ASCII-8BIT (binary) string, which
    191     # bytes presumably are of current charset encoding. we force_encoding
    192     # so that the char representation / string is tagged will be the
    193     # system locale and also hopefully the terminal/input encoding. an
    194     # incorrectly configured terminal encoding (not matching the system
    195     # encoding) will produce erronous results, but will also do that for
    196     # a lot of other programs since it is impossible to detect which is
    197     # which and what encoding the inputted byte chars are supposed to have.
    198     v.force_encoding($encoding).fix_encoding!
    199   end
    200 
    201   def remove_extra_space
    202     return nil unless @field
    203 
    204     form_driver_key Ncurses::Form::REQ_VALIDATION
    205     x = Ncurses.curx
    206     v = @field.field_buffer(0).gsub(/^\s+|\s+$/, "")
    207     v_index = x - @question.length
    208 
    209     # at start of line
    210     if v_index < 1
    211       nil
    212     ## cursor <= end of text
    213     elsif v_index < v.length
    214       # is the character before the cursor a space?
    215       if v[v_index-1] == ?\s
    216         # if there is a non-space char under cursor then go back
    217         if v[v_index] != ?\s
    218           Ncurses::Form::REQ_PREV_CHAR
    219         # otherwise delete the space
    220         else
    221           Ncurses::Form::REQ_DEL_PREV
    222         end
    223       else
    224         nil
    225       end
    226     elsif v_index == v.length
    227       # at end of string, with non-space before us
    228       nil
    229     else
    230       # trailing spaces
    231       Ncurses::Form::REQ_PREV_CHAR
    232     end
    233   end
    234 
    235   def set_cursed_value v
    236     v = "" if v.nil?
    237     @field.set_field_buffer 0, v
    238   end
    239 
    240   def cursed_value_after_point
    241     point = Ncurses.curx - @question.length
    242     get_cursed_value[point..-1]
    243   end
    244 
    245   ## this is almost certainly unnecessary, but it's the only way
    246   ## i could get ncurses to remember my form's value
    247   def nop
    248     form_driver_char " "
    249     form_driver_key Ncurses::Form::REQ_DEL_PREV
    250   end
    251 end
    252 end