interact 0.3 → 0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/interact.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # Copyright (c) 2011 Alex Suraci
2
2
 
3
3
  here = File.expand_path("../", __FILE__)
4
- require "#{here}/interact/interact"
5
4
  require "#{here}/interact/interactive"
5
+ require "#{here}/interact/rewindable"
@@ -1,82 +1,501 @@
1
- # Copyright (c) 2011 Alex Suraci
1
+ # Copyright (c) 2012 Alex Suraci
2
2
 
3
3
  module Interactive
4
- # Allow classes to enable/disable the rewind feature via +disable_rewind+
5
- # and +enable_rewind+.
6
- def self.included klass
7
- class << klass
8
- def disable_rewind
9
- def self.rewind_enabled?
10
- false
11
- end
12
- end
4
+ EVENTS = {
5
+ "\b" => :backspace,
6
+ "\t" => :tab,
7
+ "\x01" => :home,
8
+ "\x03" => :interrupt,
9
+ "\x04" => :eof,
10
+ "\x05" => :end,
11
+ "\x17" => :kill_word,
12
+ "\x7f" => :backspace,
13
+ "\r" => :enter,
14
+ "\n" => :enter
15
+ }
13
16
 
14
- def enable_rewind
15
- def self.rewind_enabled?
16
- true
17
- end
17
+ ESCAPES = {
18
+ "[A" => :up, "H" => :up,
19
+ "[B" => :down, "P" => :down,
20
+ "[C" => :right, "M" => :right,
21
+ "[D" => :left, "K" => :left,
22
+ "[3~" => :delete, "S" => :delete,
23
+ "[H" => :home, "G" => :home,
24
+ "[F" => :end, "O" => :end
25
+ }
26
+
27
+ # Wrap around the input options, the current answer, and the current
28
+ # position.
29
+ #
30
+ # Passed to handlers, which are expected to mutate +answer+ and +position+
31
+ # as they handle incoming events.
32
+ class InputState
33
+ attr_accessor :options, :answer, :position
34
+
35
+ def initialize(options = {}, answer = "", position = 0)
36
+ @options = options
37
+ @answer = answer
38
+ @position = position
39
+ @done = false
40
+ end
41
+
42
+ # Call to signal to the input reader that it can stop.
43
+ def done!
44
+ @done = true
45
+ end
46
+
47
+ # Is the input finished/complete?
48
+ def done?
49
+ @done
50
+ end
51
+
52
+ def censor(what)
53
+ if with = @options[:echo]
54
+ with * what.size
55
+ else
56
+ what
18
57
  end
58
+ end
59
+
60
+ def display(what)
61
+ print(censor(what))
62
+ @position += what.size
63
+ end
64
+
65
+ def back(x)
66
+ return if x == 0
67
+
68
+ print("\b" * (x * char_size))
69
+
70
+ @position -= x
71
+ end
72
+
73
+ def clear(x)
74
+ return if x == 0
19
75
 
20
- def rewind_enabled?
21
- true
76
+ print(" " * (x * char_size))
77
+
78
+ @position += x
79
+
80
+ back(x)
81
+ end
82
+
83
+ def goto(pos)
84
+ return if pos == position
85
+
86
+ if pos > position
87
+ display(answer[position .. pos])
88
+ else
89
+ print("\b" * (position - pos) * char_size)
22
90
  end
91
+
92
+ @position = pos
93
+ end
94
+
95
+ private
96
+
97
+ def char_size
98
+ @options[:echo] ? @options[:echo].size : 1
99
+ end
100
+ end
101
+
102
+ # Read a single character.
103
+ #
104
+ # [options] An optional hash containing the following options.
105
+ #
106
+ # input::
107
+ # The input source (defaults to <code>$stdin</code>).
108
+ def read_char(options = {})
109
+ input = options[:input] || $stdin
110
+
111
+ with_char_io(input) do
112
+ get_character(input)
23
113
  end
114
+ end
24
115
 
25
- klass.class_eval do
26
- def rewind_enabled?
27
- self.class.rewind_enabled?
116
+ # Read a single event.
117
+ #
118
+ # [options] An optional hash containing the following options.
119
+ #
120
+ # input::
121
+ # The input source (defaults to <code>$stdin</code>).
122
+ def read_event(options = {})
123
+ input = options[:input] || $stdin
124
+
125
+ with_char_io(input) do
126
+ get_event(input)
127
+ end
128
+ end
129
+
130
+ # Read a line of input.
131
+ #
132
+ # [options] An optional hash containing the following options.
133
+ #
134
+ # input::
135
+ # The input source (defaults to <code>$stdin</code>).
136
+ #
137
+ # echo::
138
+ # A string to echo when showing the input; used for things like hiding
139
+ # password input.
140
+ def read_line(options = {})
141
+ input = options[:input] || $stdin
142
+
143
+ state = input_state(options)
144
+ with_char_io(input) do
145
+ until state.done?
146
+ handler(get_event(input), state)
28
147
  end
29
148
  end
149
+
150
+ state.answer
30
151
  end
31
152
 
32
- # Ask a question and get an answer. Rewind-aware; call +disable_rewind+ on
33
- # your class to disable.
153
+ # Ask a question and get an answer.
34
154
  #
35
- # See Interact#ask for the other possible values in +options+.
155
+ # See Interact#read_line for the other possible values in +options+.
36
156
  #
37
157
  # [question] The prompt, without ": " at the end.
38
158
  #
39
159
  # [options] An optional hash containing the following options.
40
160
  #
41
- # forget::
42
- # Set to +true+ to prevent rewinding from remembering the user's answer.
43
- def ask(question, options = {}, &callback)
44
- rewind = Interact::HAS_CALLCC && rewind_enabled?
161
+ # default::
162
+ # The default value, also used to attempt type conversion of the answer
163
+ # (e.g. numeric/boolean).
164
+ #
165
+ # choices::
166
+ # An array (or +Enumerable+) of strings to choose from.
167
+ #
168
+ # indexed::
169
+ # Use alternative choice listing, and allow choosing by number. Good
170
+ # for when there are many choices or choices with long names.
171
+ def ask(question, options = {})
172
+ choices = options[:choices] && options[:choices].to_a
173
+
174
+ list_choices(choices, options) if choices
175
+
176
+ while true
177
+ prompt(question, options)
178
+ ok, res = answered(read_line(options), options)
179
+ return res if ok
180
+ end
181
+ end
182
+
183
+ private
45
184
 
46
- if rewind
47
- prompt, answer = callcc { |cc| [cc, nil] }
185
+ def clear_input(state)
186
+ state.goto(0)
187
+ state.clear(state.answer.size)
188
+ state.answer = ""
189
+ end
190
+
191
+ def set_input(state, input)
192
+ clear_input(state)
193
+ state.display(input)
194
+ state.answer = input
195
+ end
196
+
197
+ def redraw_input(state)
198
+ pos = state.position
199
+ state.goto(0)
200
+ state.display(state.answer)
201
+ state.goto(pos)
202
+ end
203
+
204
+ def input_state(options)
205
+ InputState.new(options)
206
+ end
207
+
208
+ def get_event(input)
209
+ escaped = false
210
+ escape_seq = ""
211
+
212
+ while true
213
+ c = get_character(input)
214
+
215
+ if not c
216
+ return :eof
217
+ elsif c == "\e" || c == "\xE0"
218
+ escaped = true
219
+ elsif escaped
220
+ escape_seq << c
221
+
222
+ if cmd = ESCAPES[escape_seq]
223
+ return cmd
224
+ elsif ESCAPES.select { |k, v|
225
+ k.start_with? escape_seq
226
+ }.empty?
227
+ escaped, escape_seq = false, ""
228
+ end
229
+ elsif EVENTS.key? c
230
+ return EVENTS[c]
231
+ elsif c < " "
232
+ # ignore
233
+ else
234
+ return [:key, c]
235
+ end
236
+ end
237
+ end
238
+
239
+ def answered(ans, options)
240
+ print "\n"
241
+
242
+ if ans.empty?
243
+ if options.key?(:default)
244
+ [true, options[:default]]
245
+ end
246
+ elsif choices = options[:choices]
247
+ matches = choices.select { |x| x.start_with? ans }
248
+
249
+ if matches.size == 1
250
+ [true, matches.first]
251
+ elsif choices and ans =~ /^\s*\d+\s*$/ and \
252
+ res = choices.to_a[ans.to_i - 1]
253
+ [true, res]
254
+ elsif matches.size > 1
255
+ puts "Please disambiguate: #{matches.join " or "}?"
256
+ [false, nil]
257
+ else
258
+ puts "Unknown answer, please try again!"
259
+ [false, nil]
260
+ end
48
261
  else
49
- prompt, answer = nil, nil
262
+ [true, match_type(ans, options[:default])]
50
263
  end
264
+ end
265
+
266
+ def list_choices(choices, options = {})
267
+ return unless options[:indexed]
268
+
269
+ choices.each_with_index do |o, i|
270
+ puts "#{i + 1}: #{o}"
271
+ end
272
+ end
273
+
274
+ def handler(which, state)
275
+ echo = state.options[:echo]
276
+
277
+ ans = state.answer
278
+ pos = state.position
279
+
280
+ case which
281
+ when :up
282
+ # nothing
283
+
284
+ when :down
285
+ # nothing
286
+
287
+ when :tab
288
+ if choices = state.options[:choices]
289
+ matches = choices.select { |c| c.start_with? ans }
290
+
291
+ if matches.size == 1
292
+ ans = state.answer = matches[0]
293
+ state.display(ans[pos .. -1])
294
+ else
295
+ print("\a") # bell
296
+ end
297
+ else
298
+ print("\a") # bell
299
+ end
300
+
301
+ when :right
302
+ unless pos == ans.size
303
+ state.display(ans[pos .. pos])
304
+ end
305
+
306
+ when :left
307
+ unless pos == 0
308
+ state.back(1)
309
+ end
310
+
311
+ when :delete
312
+ unless pos == ans.size
313
+ ans.slice!(pos, 1)
314
+ rest = ans[pos .. -1]
315
+ state.display(rest)
316
+ state.clear(1)
317
+ state.back(rest.size)
318
+ end
319
+
320
+ when :home
321
+ state.goto(0)
322
+
323
+ when :end
324
+ state.goto(ans.size)
325
+
326
+ when :backspace
327
+ if pos > 0
328
+ rest = ans[pos .. -1]
329
+
330
+ ans.slice!(pos - 1, 1)
331
+
332
+ state.back(1)
333
+ state.display(rest)
334
+ state.clear(1)
335
+ state.back(rest.size)
336
+ end
337
+
338
+ when :interrupt
339
+ raise Interrupt.new
340
+
341
+ when :eof
342
+ state.done! if ans.empty?
343
+
344
+ when :kill_word
345
+ if pos > 0
346
+ start = /[[:alnum:]]*\s*[^[:alnum:]]?$/ =~ ans[0 .. (pos - 1)]
347
+
348
+ if pos < ans.size
349
+ to_end = ans.size - pos
350
+ rest = ans[pos .. -1]
351
+ state.clear(to_end)
352
+ end
353
+
354
+ length = pos - start
355
+
356
+ ans.slice!(start, length)
357
+ state.back(length)
358
+ state.clear(length)
359
+
360
+ if to_end
361
+ state.display(rest)
362
+ state.back(to_end)
363
+ end
364
+ end
365
+
366
+ when :enter
367
+ state.done!
368
+
369
+ when Array
370
+ case which[0]
371
+ when :key
372
+ c = which[1]
373
+ rest = ans[pos .. -1]
374
+
375
+ ans.insert(pos, c)
376
+
377
+ state.display(c + rest)
378
+ state.back(rest.size)
379
+ end
51
380
 
52
- if answer.nil?
53
- default = options[:default]
54
381
  else
55
- default = answer
382
+ return false
56
383
  end
57
384
 
58
- prompts = (@__prompts ||= [])
385
+ true
386
+ end
387
+
388
+ def prompt(question, options = {})
389
+ print question
59
390
 
60
- callback ||= options[:callback]
391
+ if (choices = options[:choices]) && !options[:indexed]
392
+ print " (#{choices.collect(&:to_s).join ", "})"
393
+ end
61
394
 
62
- options[:prompts] = prompts
395
+ case options[:default]
396
+ when true
397
+ print " [Yn]"
398
+ when false
399
+ print " [yN]"
400
+ when nil
401
+ else
402
+ print " [#{options[:default]}]"
403
+ end
63
404
 
64
- ans = Interact.ask(question, options, &callback)
405
+ print ": "
406
+ end
65
407
 
66
- if rewind
67
- prompts << [prompt, options[:forget] ? nil : ans]
408
+ def match_type(str, x)
409
+ case x
410
+ when Integer
411
+ str.to_i
412
+ when true, false
413
+ str.upcase.start_with? "Y"
414
+ else
415
+ str
68
416
  end
417
+ end
69
418
 
70
- ans
419
+ def with_char_io(input)
420
+ before = set_input_state(input)
421
+ yield
422
+ ensure
423
+ restore_input_state(input, before)
71
424
  end
72
425
 
73
- # Clear prompts.
74
- #
75
- # Questions asked after this are rewindable, but questions asked beforehand
76
- # are no longer reachable.
77
- #
78
- # Use this after you've performed some mutation based on the user's input.
79
- def finalize
80
- @__prompts = []
426
+ def chr(x)
427
+ x && x.chr
428
+ end
429
+
430
+ private :chr
431
+
432
+ # Definitions for reading character-by-character with no echoing.
433
+ begin
434
+ require "Win32API"
435
+
436
+ def set_input_state(input)
437
+ nil
438
+ end
439
+
440
+ def restore_input_state(input, state)
441
+ nil
442
+ end
443
+
444
+ def get_character(input)
445
+ if input == STDIN
446
+ begin
447
+ chr(Win32API.new("msvcrt", "_getch", [], "L").call)
448
+ rescue
449
+ chr(Win32API.new("crtdll", "_getch", [], "L").call)
450
+ end
451
+ else
452
+ chr(input.getc)
453
+ end
454
+ end
455
+ rescue LoadError
456
+ begin
457
+ require "termios"
458
+
459
+ def set_input_state(input)
460
+ return nil unless input.tty?
461
+ before = Termio.getattr(input)
462
+
463
+ new = before.dup
464
+ new.c_lflag &= ~(Termios::ECHO | Termios::ICANON)
465
+ new.c_cc[Termios::VMIN] = 1
466
+
467
+ Termios.setattr(input, Termios::TCSANOW, new)
468
+
469
+ before
470
+ end
471
+
472
+ def restore_input_state(input, before)
473
+ if before
474
+ Termios.setattr(input, Termios::TCSANOW, before)
475
+ end
476
+ end
477
+
478
+ def get_character(input)
479
+ chr(input.getc)
480
+ end
481
+ rescue LoadError
482
+ def set_input_state(input)
483
+ return nil unless input.tty?
484
+
485
+ before = `stty -g`
486
+
487
+ system("stty -echo -icanon isig")
488
+
489
+ before
490
+ end
491
+
492
+ def restore_input_state(input, before)
493
+ system("stty #{before}") if before
494
+ end
495
+
496
+ def get_character(input)
497
+ chr(input.getc)
498
+ end
499
+ end
81
500
  end
82
501
  end
@@ -0,0 +1,97 @@
1
+ # Copyright (c) 2012 Alex Suraci
2
+
3
+ module Interactive::Rewindable
4
+ include Interactive
5
+
6
+ if defined? callcc
7
+ HAS_CALLCC = true #:nodoc:
8
+ else
9
+ begin
10
+ require "continuation"
11
+ HAS_CALLCC = true #:nodoc:
12
+ rescue LoadError
13
+ HAS_CALLCC = false #:nodoc:
14
+ end
15
+ end
16
+
17
+ class JumpToPrompt < Exception #:nodoc:
18
+ def initialize(prompt)
19
+ @prompt = prompt
20
+ end
21
+
22
+ def jump
23
+ print "\n"
24
+ @prompt[0].call(@prompt)
25
+ end
26
+ end
27
+
28
+ # Ask a question and get an answer. Rewind-aware; call +disable_rewind+ on
29
+ # your class to disable.
30
+ #
31
+ # See Interact#ask for the other possible values in +options+.
32
+ #
33
+ # [question] The prompt, without ": " at the end.
34
+ #
35
+ # [options] An optional hash containing the following options.
36
+ #
37
+ # forget::
38
+ # Set to +true+ to prevent rewinding from remembering the user's answer.
39
+ def ask(question, options = {})
40
+ rewind = HAS_CALLCC
41
+
42
+ if rewind
43
+ prompt, answer = callcc { |cc| [cc, nil] }
44
+ else
45
+ prompt, answer = nil, nil
46
+ end
47
+
48
+ if answer
49
+ options[:default] = answer
50
+ end
51
+
52
+ prompts = (@__prompts ||= [])
53
+
54
+ options[:prompts] = prompts
55
+
56
+ ans = super
57
+
58
+ if rewind
59
+ prompts << [prompt, options[:forget] ? nil : ans]
60
+ end
61
+
62
+ ans
63
+ end
64
+
65
+ # Clear prompts.
66
+ #
67
+ # Questions asked after this are rewindable, but questions asked beforehand
68
+ # are no longer reachable.
69
+ #
70
+ # Use this after you've performed some mutation based on the user's input.
71
+ def finalize
72
+ @__prompts = []
73
+ end
74
+
75
+ def handler(which, state)
76
+ prompts = state.options[:prompts] || []
77
+
78
+ case which
79
+ when :up
80
+ if back = prompts.pop
81
+ raise JumpToPrompt, back
82
+ end
83
+ end
84
+
85
+ super
86
+ end
87
+
88
+ def with_char_io(input)
89
+ before = set_input_state(input)
90
+ yield
91
+ rescue JumpToPrompt => e
92
+ restore_input_state(input, before)
93
+ e.jump
94
+ ensure
95
+ restore_input_state(input, before)
96
+ end
97
+ end
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # Copyright (c) 2011 Alex Suraci
2
2
 
3
3
  module Interact
4
- VERSION = 0.3
4
+ VERSION = 0.4
5
5
  end
metadata CHANGED
@@ -1,12 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: interact
3
3
  version: !ruby/object:Gem::Version
4
- hash: 13
4
+ hash: 3
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 3
9
- version: "0.3"
8
+ - 4
9
+ version: "0.4"
10
10
  platform: ruby
11
11
  authors:
12
12
  - Alex Suraci
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-11-08 00:00:00 -08:00
17
+ date: 2012-02-01 00:00:00 -08:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -59,8 +59,8 @@ files:
59
59
  - LICENSE
60
60
  - README.md
61
61
  - Rakefile
62
- - lib/interact/interact.rb
63
62
  - lib/interact/interactive.rb
63
+ - lib/interact/rewindable.rb
64
64
  - lib/interact.rb
65
65
  - lib/version.rb
66
66
  has_rdoc: true
@@ -1,451 +0,0 @@
1
- # Copyright (c) 2011 Alex Suraci
2
-
3
- module Interact
4
- WINDOWS = !!(RUBY_PLATFORM =~ /mingw|mswin32|cygwin/) #:nodoc:
5
-
6
- if defined? callcc
7
- HAS_CALLCC = true #:nodoc:
8
- else
9
- begin
10
- require "continuation"
11
- HAS_CALLCC = true #:nodoc:
12
- rescue LoadError
13
- HAS_CALLCC = false #:nodoc:
14
- end
15
- end
16
-
17
- EVENTS = {
18
- "\b" => :backspace,
19
- "\t" => :tab,
20
- "\x01" => :home,
21
- "\x03" => :interrupt,
22
- "\x04" => :eof,
23
- "\x05" => :end,
24
- "\x17" => :kill_word,
25
- "\x7f" => :backspace,
26
- "\r" => :enter,
27
- "\n" => :enter
28
- }
29
-
30
- ESCAPES = {
31
- "[A" => :up, "H" => :up,
32
- "[B" => :down, "P" => :down,
33
- "[C" => :right, "M" => :right,
34
- "[D" => :left, "K" => :left,
35
- "[3~" => :delete, "S" => :delete,
36
- "[H" => :home, "G" => :home,
37
- "[F" => :end, "O" => :end
38
- }
39
-
40
- class JumpToPrompt < Exception #:nodoc:
41
- def initialize(prompt)
42
- @prompt = prompt
43
- end
44
-
45
- def jump
46
- print "\n"
47
- @prompt[0].call(@prompt)
48
- end
49
- end
50
-
51
- # Wrap around the input options, the current answer, and the current
52
- # position.
53
- #
54
- # Passed to handlers, which are expected to mutate +answer+ and +position+
55
- # as they handle incoming events.
56
- class InputState
57
- attr_accessor :options, :answer, :position
58
-
59
- def initialize(options = {}, answer = "", position = 0)
60
- @options = options
61
- @answer = answer
62
- @position = position
63
- @done = false
64
- end
65
-
66
- # Call to signal to the input reader that it can stop.
67
- def done!
68
- @done = true
69
- end
70
-
71
- # Is the input finished/complete?
72
- def done?
73
- @done
74
- end
75
- end
76
-
77
- class << self
78
- # Read a single character.
79
- #
80
- # [options] An optional hash containing the following options.
81
- #
82
- # input::
83
- # The input source (defaults to <code>$stdin</code>).
84
- def read_char(options = {})
85
- input = options[:input] || $stdin
86
-
87
- with_char_io(input) do
88
- get_character(input)
89
- end
90
- end
91
-
92
- # Read a single event.
93
- #
94
- # [options] An optional hash containing the following options.
95
- #
96
- # input::
97
- # The input source (defaults to <code>$stdin</code>).
98
- #
99
- # callback::
100
- # Called with the event.
101
- def read_event(options = {}, &callback)
102
- input = options[:input] || $stdin
103
- callback ||= options[:callback]
104
-
105
- with_char_io(input) do
106
- e = get_event(input)
107
-
108
- if callback
109
- callback.call(e)
110
- else
111
- e
112
- end
113
- end
114
- end
115
-
116
- # Read a line of input.
117
- #
118
- # [options] An optional hash containing the following options.
119
- #
120
- # input::
121
- # The input source (defaults to <code>$stdin</code>).
122
- #
123
- # echo::
124
- # A string to echo when showing the input; used for things like hiding
125
- # password input.
126
- #
127
- # callback::
128
- # A block used to override certain actions.
129
- #
130
- # The block should take two arguments:
131
- #
132
- # - the event, e.g. <code>:up</code> or <code>[:key, X]</code> where
133
- # +X+ is a string containing a single character
134
- # - the +InputState+
135
- #
136
- # The block should mutate the given state, and return +true+ if it
137
- # handled the event or +false+ if it didn't.
138
- def read_line(options = {}, &callback)
139
- input = options[:input] || $stdin
140
- callback ||= options[:callback]
141
-
142
- state = InputState.new(options)
143
- with_char_io(input) do
144
- until state.done?
145
- handler(get_event(input), state, &callback)
146
- end
147
- end
148
-
149
- state.answer
150
- end
151
-
152
- # Ask a question and get an answer.
153
- #
154
- # See Interact#read_line for the other possible values in +options+.
155
- #
156
- # [question] The prompt, without ": " at the end.
157
- #
158
- # [options] An optional hash containing the following options.
159
- #
160
- # default::
161
- # The default value, also used to attempt type conversion of the answer
162
- # (e.g. numeric/boolean).
163
- #
164
- # choices::
165
- # An array (or +Enumerable+) of strings to choose from.
166
- #
167
- # indexed::
168
- # Use alternative choice listing, and allow choosing by number. Good
169
- # for when there are many choices or choices with long names.
170
- def ask(question, options = {}, &callback)
171
- default = options[:default]
172
- choices = options[:choices] && options[:choices].to_a
173
- indexed = options[:indexed]
174
- callback ||= options[:callback]
175
-
176
- if indexed
177
- choices.each_with_index do |o, i|
178
- puts "#{i + 1}: #{o}"
179
- end
180
- end
181
-
182
- while true
183
- print prompt(question, default, !indexed && choices)
184
-
185
- ans = read_line(options, &callback)
186
-
187
- print "\n"
188
-
189
- if ans.empty?
190
- return default unless default.nil?
191
- elsif choices
192
- matches = choices.select { |x| x.start_with? ans }
193
-
194
- if matches.size == 1
195
- return matches.first
196
- elsif indexed and ans =~ /^\s*\d+\s*$/ and \
197
- res = choices[ans.to_i - 1]
198
- return res
199
- elsif matches.size > 1
200
- puts "Please disambiguate: #{matches.join " or "}?"
201
- else
202
- puts "Unknown answer, please try again!"
203
- end
204
- else
205
- return match_type(ans, default)
206
- end
207
- end
208
- end
209
-
210
- private
211
-
212
- def get_event(input)
213
- escaped = false
214
- escape_seq = ""
215
-
216
- while c = get_character(input)
217
- if c == "\e" || c == "\xE0"
218
- escaped = true
219
- elsif escaped
220
- escape_seq << c
221
-
222
- if cmd = Interact::ESCAPES[escape_seq]
223
- return cmd
224
- elsif Interact::ESCAPES.select { |k, v|
225
- k.start_with? escape_seq
226
- }.empty?
227
- escaped, escape_seq = false, ""
228
- end
229
- elsif Interact::EVENTS.key? c
230
- return Interact::EVENTS[c]
231
- elsif c < " "
232
- # ignore
233
- else
234
- return [:key, c]
235
- end
236
- end
237
- end
238
-
239
- def handler(which, state)
240
- if block_given?
241
- res = yield which, state
242
- return if res
243
- end
244
-
245
- echo = state.options[:echo]
246
- prompts = state.options[:prompts] || []
247
-
248
- ans = state.answer
249
- pos = state.position
250
-
251
- case which
252
- when :up
253
- if back = prompts.pop
254
- raise Interact::JumpToPrompt, back
255
- end
256
-
257
- when :down
258
- # nothing
259
-
260
- when :tab
261
- # nothing
262
-
263
- when :right
264
- unless pos == ans.size
265
- print censor(ans[pos .. pos], echo)
266
- state.position += 1
267
- end
268
-
269
- when :left
270
- unless position == 0
271
- print "\b"
272
- state.position -= 1
273
- end
274
-
275
- when :delete
276
- unless pos == ans.size
277
- ans.slice!(pos, 1)
278
- if Interact::WINDOWS
279
- rest = ans[pos .. -1]
280
- print(censor(rest, echo) + " \b" + ("\b" * rest.size))
281
- else
282
- print("\e[P")
283
- end
284
- end
285
-
286
- when :home
287
- print("\b" * pos)
288
- state.position = 0
289
-
290
- when :end
291
- print(censor(ans[pos .. -1], echo))
292
- state.position = ans.size
293
-
294
- when :backspace
295
- if pos > 0
296
- ans.slice!(pos - 1, 1)
297
-
298
- if Interact::WINDOWS
299
- rest = ans[pos - 1 .. -1]
300
- print("\b" + censor(rest, echo) + " \b" + ("\b" * rest.size))
301
- else
302
- print("\b\e[P")
303
- end
304
-
305
- state.position -= 1
306
- end
307
-
308
- when :interrupt
309
- raise Interrupt.new
310
-
311
- when :eof
312
- state.done! if ans.empty?
313
-
314
- when :kill_word
315
- if pos > 0
316
- start = /[^\s]*\s*$/ =~ ans[0 .. pos]
317
- length = pos - start
318
- ans.slice!(start, length)
319
- print("\b" * length + " " * length + "\b" * length)
320
- state.position = start
321
- end
322
-
323
- when :enter
324
- state.done!
325
-
326
- when Array
327
- case which[0]
328
- when :key
329
- c = which[1]
330
- rest = ans[pos .. -1]
331
-
332
- ans.insert(pos, c)
333
-
334
- print(censor(c + rest, echo) + ("\b" * rest.size))
335
-
336
- state.position += 1
337
- end
338
-
339
- else
340
- return false
341
- end
342
-
343
- true
344
- end
345
-
346
- def censor(str, with)
347
- return str unless with
348
- with * str.size
349
- end
350
-
351
- def prompt(question, default = nil, choices = nil)
352
- msg = question.dup
353
-
354
- if choices
355
- msg << " (#{choices.collect(&:to_s).join ", "})"
356
- end
357
-
358
- case default
359
- when true
360
- msg << " [Yn]"
361
- when false
362
- msg << " [yN]"
363
- else
364
- msg << " [#{default}]" if default
365
- end
366
-
367
- "#{msg}: "
368
- end
369
-
370
- def match_type(str, x)
371
- case x
372
- when Integer
373
- str.to_i
374
- when true, false
375
- str.upcase.start_with? "Y"
376
- else
377
- str
378
- end
379
- end
380
-
381
- # Definitions for reading character-by-character with no echoing.
382
- begin
383
- require "Win32API"
384
-
385
- def with_char_io(input)
386
- yield
387
- rescue Interact::JumpToPrompt => e
388
- e.jump
389
- end
390
-
391
- def get_character(input)
392
- if input == STDIN
393
- begin
394
- Win32API.new("msvcrt", "_getch", [], "L").call.chr
395
- rescue
396
- Win32API.new("crtdll", "_getch", [], "L").call.chr
397
- end
398
- else
399
- input.getc.chr
400
- end
401
- end
402
- rescue LoadError
403
- begin
404
- require "termios"
405
-
406
- def with_char_io(input)
407
- return yield unless input.tty?
408
-
409
- before = Termios.getattr(input)
410
-
411
- new = before.dup
412
- new.c_lflag &= ~(Termios::ECHO | Termios::ICANON)
413
- new.c_cc[Termios::VMIN] = 1
414
-
415
- begin
416
- Termios.setattr(input, Termios::TCSANOW, new)
417
- yield
418
- rescue Interact::JumpToPrompt => e
419
- Termios.setattr(input, Termios::TCSANOW, before)
420
- e.jump
421
- ensure
422
- Termios.setattr(input, Termios::TCSANOW, before)
423
- end
424
- end
425
-
426
- def get_character(input)
427
- input.getc.chr
428
- end
429
- rescue LoadError
430
- def with_char_io(input)
431
- return yield unless input.tty?
432
-
433
- begin
434
- before = `stty -g`
435
- system("stty -echo -icanon isig")
436
- yield
437
- rescue Interact::JumpToPrompt => e
438
- system("stty #{before}")
439
- e.jump
440
- ensure
441
- system("stty #{before}")
442
- end
443
- end
444
-
445
- def get_character(input)
446
- input.getc.chr
447
- end
448
- end
449
- end
450
- end
451
- end