class Redwood::TextField

a fully-functional text field supporting completions, expansions, history–everything!

writing this fucking sucked. if you thought ncurses was some 1970s before-people-knew-how-to-program bullshit, wait till you see ncurses forms.

completion comments: completion is done emacs-style, and mostly depends on outside support, as we merely signal the existence of a new set of completions to show (new_completions?) or that the current list of completions should be rolled if they're too large to fill the screen (roll_completions?).

in sup, completion support is implemented through BufferManager#ask and CompletionMode.

Attributes

completions[R]

Public Class Methods

new() click to toggle source
# File lib/sup/textfield.rb, line 23
def initialize
  @i = nil
  @history = []

  @completion_block = nil
  reset_completion_state
end

Public Instance Methods

activate(window, y, x, width, question, default=nil, &block) click to toggle source
# File lib/sup/textfield.rb, line 36
def activate window, y, x, width, question, default=nil, &block
  @w, @y, @x, @width = window, y, x, width
  @question = question
  @completion_block = block
  @field = Ncurses::Form.new_field 1, @width - question.length, @y, @x + question.length, 0, 0
  if @field.respond_to? :opts_off
    @field.opts_off Ncurses::Form::O_STATIC
    @field.opts_off Ncurses::Form::O_BLANK
  end
  @form = Ncurses::Form.new_form [@field]
  @value = default || ''
  Ncurses::Form.post_form @form
  set_cursed_value @value
end
deactivate() click to toggle source
# File lib/sup/textfield.rb, line 59
def deactivate
  reset_completion_state
  @form.unpost_form
  @form.free_form
  @field.free_field
  @field = nil
  Ncurses.curs_set 0
end
handle_input(c) click to toggle source
# File lib/sup/textfield.rb, line 68
def handle_input c
  ## short-circuit exit paths
  case c.code
  when Ncurses::KEY_ENTER # submit!
    @value = get_cursed_value
    @history.push @value unless @value =~ /^\s*$/
    @i = @history.size
    return false
  when Ncurses::KEY_CANCEL # cancel
    @value = nil
    return false
  when Ncurses::KEY_TAB # completion
    return true unless @completion_block
    if @completions.empty?
      v = get_cursed_value
      c = @completion_block.call v
      if c.size > 0
        @value = c.map { |full, short| full }.shared_prefix(true)
        set_cursed_value @value
        position_cursor
      end
      if c.size > 1
        @completions = c
        @new_completions = true
        @roll_completions = false
      end
    else
      @new_completions = false
      @roll_completions = true
    end
    return true
  end

  reset_completion_state
  @value = nil

  # 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
      Ncurses::Form::REQ_DEL_PREV
    when Ncurses::KEY_HOME
      nop
      Ncurses::Form::REQ_BEG_FIELD
    when Ncurses::KEY_END
      Ncurses::Form::REQ_END_FIELD
    when Ncurses::KEY_UP, Ncurses::KEY_DOWN
      unless !@i || @history.empty?
        value = get_cursed_value
        #debug "history before #{@history.inspect}"
        @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] || ''
        #debug "history after #{@history.inspect}"
        set_cursed_value @value
        Ncurses::Form::REQ_END_FIELD
      end
    else
      # return other keycode or nil if it's not a keycode
      c.dumb? ? nil : c.keycode
    end

  # 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
position_cursor() click to toggle source
# File lib/sup/textfield.rb, line 51
def position_cursor
  @w.attrset Colormap.color_for(:none)
  @w.mvaddstr @y, 0, @question
  Ncurses.curs_set 1
  form_driver_key Ncurses::Form::REQ_END_FIELD
  form_driver_key Ncurses::Form::REQ_NEXT_CHAR if @value && @value =~ / $/ # fucking RETARDED
end
value() click to toggle source
# File lib/sup/textfield.rb, line 34
def value; @value || get_cursed_value end

Private Instance Methods

cursed_value_after_point() click to toggle source
# File lib/sup/textfield.rb, line 241
def cursed_value_after_point
  point = Ncurses.curx - @question.length
  get_cursed_value[point..-1]
end
get_cursed_value() click to toggle source

ncurses inanity wrapper

DO NOT READ THIS CODE. YOU WILL GO MAD.

# File lib/sup/textfield.rb, line 177
def get_cursed_value
  return nil unless @field

  x = Ncurses.curx
  form_driver_key Ncurses::Form::REQ_VALIDATION
  v = @field.field_buffer(0).gsub(/^\s+|\s+$/, "")

  ## cursor <= end of text
  if x - @question.length - v.length <= 0
    v
  else # trailing spaces
    v + (" " * (x - @question.length - v.length))
  end

  # ncurses returns a ASCII-8BIT (binary) string, which
  # bytes presumably are of current charset encoding. we force_encoding
  # so that the char representation / string is tagged will be the
  # 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 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
nop() click to toggle source

this is almost certainly unnecessary, but it's the only way i could get ncurses to remember my form's value

# File lib/sup/textfield.rb, line 248
def nop
  form_driver_char " "
  form_driver_key Ncurses::Form::REQ_DEL_PREV
end
remove_extra_space() click to toggle source
# File lib/sup/textfield.rb, line 202
def remove_extra_space
  return nil unless @field

  form_driver_key Ncurses::Form::REQ_VALIDATION
  x = Ncurses.curx
  v = @field.field_buffer(0).gsub(/^\s+|\s+$/, "")
  v_index = x - @question.length

  # at start of line
  if v_index < 1
    nil
  ## cursor <= end of text
  elsif v_index < v.length
    # is the character before the cursor a space?
    if v[v_index-1] == ?\s
      # if there is a non-space char under cursor then go back
      if v[v_index] != ?\s
        Ncurses::Form::REQ_PREV_CHAR
      # otherwise delete the space
      else
        Ncurses::Form::REQ_DEL_PREV
      end
    else
      nil
    end
  elsif v_index == v.length
    # at end of string, with non-space before us
    nil
  else
    # trailing spaces
    Ncurses::Form::REQ_PREV_CHAR
  end
end
reset_completion_state() click to toggle source
# File lib/sup/textfield.rb, line 169
def reset_completion_state
  @completions = []
  @new_completions = @roll_completions = @clear_completions = false
end
set_cursed_value(v) click to toggle source
# File lib/sup/textfield.rb, line 236
def set_cursed_value v
  v = "" if v.nil?
  @field.set_field_buffer 0, v
end