interact 0.3 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
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