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