module Redwood ## 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. class TextField def initialize @i = nil @history = [] @completion_block = nil reset_completion_state end bool_reader :new_completions, :roll_completions attr_reader :completions def value; @value || get_cursed_value end 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 def position_cursor @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 end def deactivate reset_completion_state @form.unpost_form @form.free_form @field.free_field @field = nil Ncurses.curs_set 0 end def handle_input c ## short-circuit exit paths case c 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 d = case c 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 Ncurses::Form::REQ_DEL_PREV when ?\C-a.ord, Ncurses::KEY_HOME nop Ncurses::Form::REQ_BEG_FIELD when ?\C-e.ord, 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 = 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 c end Ncurses::Form.form_driver @form, d if d true end private def reset_completion_state @completions = [] @new_completions = @roll_completions = @clear_completions = false end ## ncurses inanity wrapper ## ## DO NOT READ THIS CODE. YOU WILL GO MAD. def get_cursed_value return nil unless @field x = Ncurses.curx Ncurses::Form.form_driver @form, 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 log 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 def remove_extra_space return nil unless @field Ncurses::Form.form_driver @form, 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 def set_cursed_value v @field.set_field_buffer 0, v end def cursed_value_after_point point = Ncurses.curx - @question.length get_cursed_value[point..-1] end ## 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 end end end