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 +1 -1
- data/lib/interact/interactive.rb +466 -47
- data/lib/interact/rewindable.rb +97 -0
- data/lib/version.rb +1 -1
- metadata +5 -5
- data/lib/interact/interact.rb +0 -451
data/lib/interact.rb
CHANGED
data/lib/interact/interactive.rb
CHANGED
@@ -1,82 +1,501 @@
|
|
1
|
-
# Copyright (c)
|
1
|
+
# Copyright (c) 2012 Alex Suraci
|
2
2
|
|
3
3
|
module Interactive
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
21
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
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.
|
33
|
-
# your class to disable.
|
153
|
+
# Ask a question and get an answer.
|
34
154
|
#
|
35
|
-
# See Interact#
|
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
|
-
#
|
42
|
-
#
|
43
|
-
|
44
|
-
|
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
|
-
|
47
|
-
|
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
|
-
|
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
|
-
|
382
|
+
return false
|
56
383
|
end
|
57
384
|
|
58
|
-
|
385
|
+
true
|
386
|
+
end
|
387
|
+
|
388
|
+
def prompt(question, options = {})
|
389
|
+
print question
|
59
390
|
|
60
|
-
|
391
|
+
if (choices = options[:choices]) && !options[:indexed]
|
392
|
+
print " (#{choices.collect(&:to_s).join ", "})"
|
393
|
+
end
|
61
394
|
|
62
|
-
options[:
|
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
|
-
|
405
|
+
print ": "
|
406
|
+
end
|
65
407
|
|
66
|
-
|
67
|
-
|
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
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
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:
|
4
|
+
hash: 3
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: "0.
|
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:
|
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
|
data/lib/interact/interact.rb
DELETED
@@ -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
|